Skip to content

Commit 271796b

Browse files
committed
feat: model info display and compaction model setting in Settings
- Add contextWindow and description fields to KnownModelDefinition - Populate context window sizes and descriptions for all built-in models - Display context window size (e.g., 200K, 1M) in model rows - Add info tooltip with model description for built-in models - Add Compaction Model dropdown in Model Defaults section - Remove star icons from model rows (default model set via dropdown) - Unified Model Defaults section with Default Model and Compaction Model
1 parent 4c0b368 commit 271796b

File tree

3 files changed

+186
-37
lines changed

3 files changed

+186
-37
lines changed

src/browser/components/Settings/sections/ModelRow.tsx

Lines changed: 31 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,29 @@
11
import React from "react";
2-
import { Check, Eye, Pencil, Star, Trash2, X } from "lucide-react";
2+
import { Check, Eye, Info, Pencil, Trash2, X } from "lucide-react";
33
import { createEditKeyHandler } from "@/browser/utils/ui/keybinds";
44
import { GatewayIcon } from "@/browser/components/icons/GatewayIcon";
55
import { cn } from "@/common/lib/utils";
66
import { Tooltip, TooltipTrigger, TooltipContent } from "@/browser/components/ui/tooltip";
77
import { ProviderWithIcon } from "@/browser/components/ProviderIcon";
88
import { Button } from "@/browser/components/ui/button";
99

10+
/** Format token count as human-readable string (e.g., 200000 -> "200K") */
11+
function formatTokenCount(tokens: number): string {
12+
if (tokens >= 1_000_000) {
13+
return `${(tokens / 1_000_000).toFixed(tokens % 1_000_000 === 0 ? 0 : 1)}M`;
14+
}
15+
if (tokens >= 1_000) {
16+
return `${(tokens / 1_000).toFixed(tokens % 1_000 === 0 ? 0 : 1)}K`;
17+
}
18+
return tokens.toString();
19+
}
20+
1021
export interface ModelRowProps {
1122
provider: string;
1223
modelId: string;
1324
fullId: string;
1425
aliases?: string[];
1526
isCustom: boolean;
16-
isDefault: boolean;
1727
isEditing: boolean;
1828
editValue?: string;
1929
editError?: string | null;
@@ -23,7 +33,10 @@ export interface ModelRowProps {
2333
isGatewayEnabled?: boolean;
2434
/** Whether this model is hidden from the selector */
2535
isHiddenFromSelector?: boolean;
26-
onSetDefault: () => void;
36+
/** Context window size in tokens */
37+
contextWindow?: number;
38+
/** Brief description of the model */
39+
description?: string;
2740
onStartEdit?: () => void;
2841
onSaveEdit?: () => void;
2942
onCancelEdit?: () => void;
@@ -74,6 +87,21 @@ export function ModelRow(props: ModelRowProps) {
7487
<TooltipContent align="center">Use with /m {props.aliases[0]}</TooltipContent>
7588
</Tooltip>
7689
)}
90+
{props.contextWindow && (
91+
<span className="text-muted-light shrink-0 text-[10px]">
92+
{formatTokenCount(props.contextWindow)}
93+
</span>
94+
)}
95+
{props.description && (
96+
<Tooltip>
97+
<TooltipTrigger asChild>
98+
<Info className="text-muted-light h-3 w-3 shrink-0 cursor-help" />
99+
</TooltipTrigger>
100+
<TooltipContent align="center" className="max-w-xs">
101+
{props.description}
102+
</TooltipContent>
103+
</Tooltip>
104+
)}
77105
</div>
78106
)}
79107
</div>
@@ -163,31 +191,6 @@ export function ModelRow(props: ModelRowProps) {
163191
</TooltipContent>
164192
</Tooltip>
165193
)}
166-
{/* Favorite/default button */}
167-
<Tooltip>
168-
<TooltipTrigger asChild>
169-
<Button
170-
variant="ghost"
171-
size="icon"
172-
onClick={() => {
173-
if (!props.isDefault) props.onSetDefault();
174-
}}
175-
className={cn(
176-
"h-6 w-6",
177-
props.isDefault
178-
? "cursor-default text-yellow-400 hover:text-yellow-400"
179-
: "text-muted hover:text-yellow-400"
180-
)}
181-
disabled={props.isDefault}
182-
aria-label={props.isDefault ? "Current default model" : "Set as default model"}
183-
>
184-
<Star className={cn("h-3.5 w-3.5", props.isDefault && "fill-current")} />
185-
</Button>
186-
</TooltipTrigger>
187-
<TooltipContent align="center">
188-
{props.isDefault ? "Default model" : "Set as default"}
189-
</TooltipContent>
190-
</Tooltip>
191194
{/* Edit/delete buttons only for custom models */}
192195
{props.isCustom && (
193196
<>

src/browser/components/Settings/sections/ModelsSection.tsx

Lines changed: 127 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useState, useCallback } from "react";
1+
import React, { useState, useCallback, useEffect, useRef } from "react";
22
import { Plus, Loader2 } from "lucide-react";
33
import { SUPPORTED_PROVIDERS, PROVIDER_DISPLAY_NAMES } from "@/common/constants/providers";
44
import { KNOWN_MODELS } from "@/common/constants/knownModels";
@@ -15,6 +15,10 @@ import {
1515
SelectValue,
1616
} from "@/browser/components/ui/select";
1717
import { Button } from "@/browser/components/ui/button";
18+
import { ModelSelector } from "@/browser/components/ModelSelector";
19+
import { MODE_AI_DEFAULTS_KEY } from "@/common/constants/storage";
20+
import { normalizeModeAiDefaults, type ModeAiDefaults } from "@/common/types/modeAiDefaults";
21+
import { updatePersistedState } from "@/browser/hooks/usePersistedState";
1822

1923
// Providers to exclude from the custom models UI (handled specially or internal)
2024
const HIDDEN_PROVIDERS = new Set(["mux-gateway"]);
@@ -37,13 +41,97 @@ export function ModelsSection() {
3741
const [editing, setEditing] = useState<EditingState | null>(null);
3842
const [error, setError] = useState<string | null>(null);
3943

44+
// Compaction model state
45+
const [compactionModel, setCompactionModelState] = useState<string>("");
46+
const [compactionLoaded, setCompactionLoaded] = useState(false);
47+
const compactionSaveTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
48+
4049
const selectableProviders = SUPPORTED_PROVIDERS.filter(
4150
(provider) => !HIDDEN_PROVIDERS.has(provider)
4251
);
43-
const { defaultModel, setDefaultModel, hiddenModels, hideModel, unhideModel } =
52+
const { models, defaultModel, setDefaultModel, hiddenModels, hideModel, unhideModel } =
4453
useModelsFromSettings();
4554
const gateway = useGateway();
4655

56+
// Load compaction model from config on mount
57+
useEffect(() => {
58+
if (!api) return;
59+
60+
void api.config
61+
.getConfig()
62+
.then((cfg) => {
63+
const normalized = normalizeModeAiDefaults(cfg.modeAiDefaults ?? {});
64+
setCompactionModelState(normalized.compact?.modelString ?? "");
65+
setCompactionLoaded(true);
66+
})
67+
.catch(() => {
68+
setCompactionLoaded(true);
69+
});
70+
}, [api]);
71+
72+
// Debounced save for compaction model changes
73+
const setCompactionModel = useCallback(
74+
(model: string) => {
75+
setCompactionModelState(model);
76+
77+
// Clear any pending save
78+
if (compactionSaveTimerRef.current) {
79+
clearTimeout(compactionSaveTimerRef.current);
80+
}
81+
82+
compactionSaveTimerRef.current = setTimeout(() => {
83+
if (!api) return;
84+
85+
// Update local cache immediately for non-React readers
86+
updatePersistedState<ModeAiDefaults>(
87+
MODE_AI_DEFAULTS_KEY,
88+
(prev) => {
89+
const next = { ...prev };
90+
if (!model) {
91+
if (next.compact) {
92+
delete next.compact.modelString;
93+
if (!next.compact.thinkingLevel) delete next.compact;
94+
}
95+
} else {
96+
next.compact = { ...next.compact, modelString: model };
97+
}
98+
return next;
99+
},
100+
{}
101+
);
102+
103+
// Persist to backend
104+
void api.config.getConfig().then((cfg) => {
105+
const existing = normalizeModeAiDefaults(cfg.modeAiDefaults ?? {});
106+
const updated: ModeAiDefaults = { ...existing };
107+
108+
if (!model) {
109+
if (updated.compact) {
110+
delete updated.compact.modelString;
111+
if (!updated.compact.thinkingLevel) {
112+
delete updated.compact;
113+
}
114+
}
115+
} else {
116+
updated.compact = { ...updated.compact, modelString: model };
117+
}
118+
119+
void api.config.updateModeAiDefaults({ modeAiDefaults: updated });
120+
});
121+
}, 400);
122+
},
123+
[api]
124+
);
125+
126+
// Cleanup save timer on unmount
127+
useEffect(() => {
128+
return () => {
129+
if (compactionSaveTimerRef.current) {
130+
clearTimeout(compactionSaveTimerRef.current);
131+
}
132+
};
133+
}, []);
134+
47135
// Check if a model already exists (for duplicate prevention)
48136
const modelExists = useCallback(
49137
(provider: string, modelId: string, excludeOriginal?: string): boolean => {
@@ -169,15 +257,47 @@ export function ModelsSection() {
169257
modelId: model.providerModelId,
170258
fullId: model.id,
171259
aliases: model.aliases,
260+
contextWindow: model.contextWindow,
261+
description: model.description,
172262
}));
173263

174264
const customModels = getCustomModels();
175265

176266
return (
177267
<div className="space-y-4">
178-
<p className="text-muted text-xs">
179-
Manage your models. Click the star to set a default model for new workspaces.
180-
</p>
268+
{/* Model Defaults */}
269+
{compactionLoaded && (
270+
<div className="border-border-medium bg-background-secondary rounded-md border p-3">
271+
<div className="text-foreground mb-3 text-sm font-medium">Model Defaults</div>
272+
273+
{/* Default Model */}
274+
<div className="mb-4 space-y-1">
275+
<div className="text-muted text-xs">Default Model</div>
276+
<ModelSelector
277+
value={defaultModel}
278+
onChange={setDefaultModel}
279+
models={models}
280+
hiddenModels={hiddenModels}
281+
/>
282+
<div className="text-muted-light text-[10px]">Used for new workspaces</div>
283+
</div>
284+
285+
{/* Compaction Model */}
286+
<div className="space-y-1">
287+
<div className="text-muted text-xs">Compaction Model</div>
288+
<ModelSelector
289+
value={compactionModel}
290+
emptyLabel="Use workspace model"
291+
onChange={setCompactionModel}
292+
models={models}
293+
hiddenModels={hiddenModels}
294+
/>
295+
<div className="text-muted-light text-[10px]">
296+
Model used for compacting history. Falls back to workspace model if not set.
297+
</div>
298+
</div>
299+
</div>
300+
)}
181301

182302
{/* Custom Models - shown first */}
183303
<div className="space-y-1.5">
@@ -236,14 +356,12 @@ export function ModelsSection() {
236356
modelId={model.modelId}
237357
fullId={model.fullId}
238358
isCustom={true}
239-
isDefault={defaultModel === model.fullId}
240359
isEditing={isModelEditing}
241360
editValue={isModelEditing ? editing.newModelId : undefined}
242361
editError={isModelEditing ? error : undefined}
243362
saving={false}
244363
hasActiveEdit={editing !== null}
245364
isGatewayEnabled={gateway.modelUsesGateway(model.fullId)}
246-
onSetDefault={() => setDefaultModel(model.fullId)}
247365
onStartEdit={() => handleStartEdit(model.provider, model.modelId)}
248366
onSaveEdit={handleSaveEdit}
249367
onCancelEdit={handleCancelEdit}
@@ -279,11 +397,11 @@ export function ModelsSection() {
279397
modelId={model.modelId}
280398
fullId={model.fullId}
281399
aliases={model.aliases}
400+
contextWindow={model.contextWindow}
401+
description={model.description}
282402
isCustom={false}
283-
isDefault={defaultModel === model.fullId}
284403
isEditing={false}
285404
isGatewayEnabled={gateway.modelUsesGateway(model.fullId)}
286-
onSetDefault={() => setDefaultModel(model.fullId)}
287405
isHiddenFromSelector={hiddenModels.includes(model.fullId)}
288406
onToggleVisibility={() =>
289407
hiddenModels.includes(model.fullId)

0 commit comments

Comments
 (0)