Skip to content

Commit 17314cb

Browse files
ENG-317 / Feat: Add model "favorites" toggle for Cline & OpenRouter providers (RooCodeInc#2722)
* initial * Still facing issue with OR/Cline provider switching * Working with provider switches * cleanup * cleanup * changset * Update .changeset/twelve-rocks-drum.md Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> * test (in progress) * added telemetry * removed test * cleanup --------- Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
1 parent 6f9cf8a commit 17314cb

File tree

10 files changed

+133
-19
lines changed

10 files changed

+133
-19
lines changed

.changeset/neat-wolves-grab.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"claude-dev": patch
3+
---
4+
5+
Added telemetry for model favoriting

.changeset/twelve-rocks-drum.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"claude-dev": minor
3+
---
4+
5+
Adds favorite toggles for models when using the Cline & OpenRouter providers

src/core/controller/index.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -889,6 +889,27 @@ export class Controller {
889889
}
890890
break
891891
}
892+
case "toggleFavoriteModel": {
893+
if (message.modelId) {
894+
const { apiConfiguration } = await getAllExtensionState(this.context)
895+
const favoritedModelIds = apiConfiguration.favoritedModelIds || []
896+
897+
// Toggle favorite status
898+
const updatedFavorites = favoritedModelIds.includes(message.modelId)
899+
? favoritedModelIds.filter((id) => id !== message.modelId)
900+
: [...favoritedModelIds, message.modelId]
901+
902+
await updateGlobalState(this.context, "favoritedModelIds", updatedFavorites)
903+
904+
// Capture telemetry for model favorite toggle
905+
const isFavorited = !favoritedModelIds.includes(message.modelId)
906+
telemetryService.captureModelFavoritesUsage(message.modelId, isFavorited)
907+
908+
// Post state to webview without changing any other configuration
909+
await this.postStateToWebview()
910+
}
911+
break
912+
}
892913
// Add more switch case statements here as more webview message commands
893914
// are created within the webview context (i.e. inside media/main.js)
894915
}

src/core/storage/state-keys.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,3 +67,4 @@ export type GlobalStateKey =
6767
| "asksageApiUrl"
6868
| "thinkingBudgetTokens"
6969
| "planActSeparateModelsSetting"
70+
| "favoritedModelIds"

src/core/storage/state.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
117117
thinkingBudgetTokens,
118118
sambanovaApiKey,
119119
planActSeparateModelsSettingRaw,
120+
favoritedModelIds,
120121
] = await Promise.all([
121122
getGlobalState(context, "apiProvider") as Promise<ApiProvider | undefined>,
122123
getGlobalState(context, "apiModelId") as Promise<string | undefined>,
@@ -183,6 +184,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
183184
getGlobalState(context, "thinkingBudgetTokens") as Promise<number | undefined>,
184185
getSecret(context, "sambanovaApiKey") as Promise<string | undefined>,
185186
getGlobalState(context, "planActSeparateModelsSetting") as Promise<boolean | undefined>,
187+
getGlobalState(context, "favoritedModelIds") as Promise<string[] | undefined>,
186188
])
187189

188190
let apiProvider: ApiProvider
@@ -275,6 +277,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
275277
asksageApiUrl,
276278
xaiApiKey,
277279
sambanovaApiKey,
280+
favoritedModelIds,
278281
},
279282
lastShownAnnouncementId,
280283
customInstructions,
@@ -347,6 +350,7 @@ export async function updateApiConfiguration(context: vscode.ExtensionContext, a
347350
thinkingBudgetTokens,
348351
clineApiKey,
349352
sambanovaApiKey,
353+
favoritedModelIds,
350354
} = apiConfiguration
351355
await updateGlobalState(context, "apiProvider", apiProvider)
352356
await updateGlobalState(context, "apiModelId", apiModelId)
@@ -399,6 +403,7 @@ export async function updateApiConfiguration(context: vscode.ExtensionContext, a
399403
await updateGlobalState(context, "thinkingBudgetTokens", thinkingBudgetTokens)
400404
await storeSecret(context, "clineApiKey", clineApiKey)
401405
await storeSecret(context, "sambanovaApiKey", sambanovaApiKey)
406+
await updateGlobalState(context, "favoritedModelIds", favoritedModelIds)
402407
}
403408

404409
export async function resetExtensionState(context: vscode.ExtensionContext) {

src/services/telemetry/TelemetryService.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ class PostHogClient {
7171
PLAN_MODE_TOGGLED: "ui.plan_mode_toggled",
7272
// Tracks when action mode is toggled on
7373
ACT_MODE_TOGGLED: "ui.act_mode_toggled",
74+
// Tracks when users use the "favorite" button in the model picker
75+
MODEL_FAVORITE_TOGGLED: "ui.model_favorite_toggled",
7476
},
7577
}
7678

@@ -574,6 +576,21 @@ class PostHogClient {
574576
})
575577
}
576578

579+
/**
580+
* Records when the user uses the model favorite button in the model picker
581+
* @param model The name of the model the user has interacted with
582+
* @param isFavorited Whether the model is being favorited (true) or unfavorited (false)
583+
*/
584+
public captureModelFavoritesUsage(model: string, isFavorited: boolean) {
585+
this.capture({
586+
event: PostHogClient.EVENTS.UI.MODEL_FAVORITE_TOGGLED,
587+
properties: {
588+
model,
589+
isFavorited,
590+
},
591+
})
592+
}
593+
577594
public isTelemetryEnabled(): boolean {
578595
return this.telemetryEnabled
579596
}

src/shared/WebviewMessage.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ export interface WebviewMessage {
7979
| "scrollToSettings"
8080
| "getRelativePaths" // Handles single and multiple URI resolution
8181
| "searchFiles"
82+
| "toggleFavoriteModel"
8283
// | "relaunchChromeDebugMode"
8384
text?: string
8485
uris?: string[] // Used for getRelativePaths
@@ -113,6 +114,8 @@ export interface WebviewMessage {
113114
feedbackType?: TaskFeedbackType
114115
mentionsRequestId?: string
115116
query?: string
117+
// For toggleFavoriteModel
118+
modelId?: string
116119
}
117120

118121
export type ClineAskResponse = "yesButtonClicked" | "noButtonClicked" | "messageResponse"

src/shared/api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ export interface ApiHandlerOptions {
7878

7979
export type ApiConfiguration = ApiHandlerOptions & {
8080
apiProvider?: ApiProvider
81+
favoritedModelIds?: string[]
8182
}
8283

8384
// Models

webview-ui/src/components/settings/ApiOptions.tsx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,10 +101,25 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
101101
const [providerSortingSelected, setProviderSortingSelected] = useState(!!apiConfiguration?.openRouterProviderSorting)
102102

103103
const handleInputChange = (field: keyof ApiConfiguration) => (event: any) => {
104+
const newValue = event.target.value
105+
106+
// Update local state
104107
setApiConfiguration({
105108
...apiConfiguration,
106-
[field]: event.target.value,
109+
[field]: newValue,
107110
})
111+
112+
// If the field is the provider, save it immediately
113+
// Neccesary for favorite model selection to work without undoing provider changes
114+
if (field === "apiProvider") {
115+
vscode.postMessage({
116+
type: "apiConfiguration",
117+
apiConfiguration: {
118+
...apiConfiguration,
119+
apiProvider: newValue,
120+
},
121+
})
122+
}
108123
}
109124

110125
const { selectedProvider, selectedModelId, selectedModelInfo } = useMemo(() => {

webview-ui/src/components/settings/OpenRouterModelPicker.tsx

Lines changed: 59 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,25 @@ import { CODE_BLOCK_BG_COLOR } from "@/components/common/CodeBlock"
1313
import ThinkingBudgetSlider from "./ThinkingBudgetSlider"
1414
import FeaturedModelCard from "./FeaturedModelCard"
1515

16+
// Star icon for favorites
17+
const StarIcon = ({ isFavorite, onClick }: { isFavorite: boolean; onClick: (e: React.MouseEvent) => void }) => {
18+
return (
19+
<div
20+
onClick={onClick}
21+
style={{
22+
cursor: "pointer",
23+
color: isFavorite ? "var(--vscode-terminal-ansiBlue)" : "var(--vscode-descriptionForeground)",
24+
marginLeft: "8px",
25+
fontSize: "16px",
26+
display: "flex",
27+
alignItems: "center",
28+
justifyContent: "center",
29+
}}>
30+
{isFavorite ? "★" : "☆"}
31+
</div>
32+
)
33+
}
34+
1635
export interface OpenRouterModelPickerProps {
1736
isPopup?: boolean
1837
}
@@ -110,9 +129,18 @@ const OpenRouterModelPicker: React.FC<OpenRouterModelPickerProps> = ({ isPopup }
110129
const results: { id: string; html: string }[] = searchTerm
111130
? highlight(fuse.search(searchTerm), "model-item-highlight")
112131
: searchableItems
113-
// results.sort((a, b) => a.id.localeCompare(b.id)) NOTE: sorting like this causes ids in objects to be reordered and mismatched
114-
return results
115-
}, [searchableItems, searchTerm, fuse])
132+
133+
// Sort favorited models to the top
134+
const favoritedModelIds = apiConfiguration?.favoritedModelIds || []
135+
return results.sort((a, b) => {
136+
const aIsFavorite = favoritedModelIds.includes(a.id)
137+
const bIsFavorite = favoritedModelIds.includes(b.id)
138+
139+
if (aIsFavorite && !bIsFavorite) return -1
140+
if (!aIsFavorite && bIsFavorite) return 1
141+
return a.id.localeCompare(b.id)
142+
})
143+
}, [searchableItems, searchTerm, fuse, apiConfiguration?.favoritedModelIds])
116144

117145
const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
118146
if (!isDropdownVisible) return
@@ -241,21 +269,34 @@ const OpenRouterModelPicker: React.FC<OpenRouterModelPickerProps> = ({ isPopup }
241269
</VSCodeTextField>
242270
{isDropdownVisible && (
243271
<DropdownList ref={dropdownListRef}>
244-
{modelSearchResults.map((item, index) => (
245-
<DropdownItem
246-
key={item.id}
247-
ref={(el) => (itemRefs.current[index] = el)}
248-
isSelected={index === selectedIndex}
249-
onMouseEnter={() => setSelectedIndex(index)}
250-
onClick={() => {
251-
handleModelChange(item.id)
252-
setIsDropdownVisible(false)
253-
}}
254-
dangerouslySetInnerHTML={{
255-
__html: item.html,
256-
}}
257-
/>
258-
))}
272+
{modelSearchResults.map((item, index) => {
273+
const isFavorite = (apiConfiguration?.favoritedModelIds || []).includes(item.id)
274+
return (
275+
<DropdownItem
276+
key={item.id}
277+
ref={(el) => (itemRefs.current[index] = el)}
278+
isSelected={index === selectedIndex}
279+
onMouseEnter={() => setSelectedIndex(index)}
280+
onClick={() => {
281+
handleModelChange(item.id)
282+
setIsDropdownVisible(false)
283+
}}>
284+
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
285+
<span dangerouslySetInnerHTML={{ __html: item.html }} />
286+
<StarIcon
287+
isFavorite={isFavorite}
288+
onClick={(e) => {
289+
e.stopPropagation()
290+
vscode.postMessage({
291+
type: "toggleFavoriteModel",
292+
modelId: item.id,
293+
})
294+
}}
295+
/>
296+
</div>
297+
</DropdownItem>
298+
)
299+
})}
259300
</DropdownList>
260301
)}
261302
</DropdownWrapper>

0 commit comments

Comments
 (0)