Skip to content

Commit fd67858

Browse files
authored
fix: bring back model selection (#844)
1 parent dbe6308 commit fd67858

7 files changed

Lines changed: 149 additions & 4 deletions

File tree

apps/twig/src/renderer/features/sessions/stores/sessionStore.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ interface SessionActions {
149149
initialPrompt?: ContentBlock[];
150150
executionMode?: string;
151151
adapter?: "claude" | "codex";
152+
model?: string;
152153
}) => Promise<void>;
153154
disconnectFromTask: (taskId: string) => Promise<void>;
154155
sendPrompt: (
@@ -819,6 +820,7 @@ const useStore = create<SessionStore>()(
819820
initialPrompt?: ContentBlock[],
820821
executionMode?: string,
821822
adapter?: "claude" | "codex",
823+
model?: string,
822824
) => {
823825
if (!auth.client) {
824826
throw new Error(
@@ -871,12 +873,12 @@ const useStore = create<SessionStore>()(
871873
);
872874
}
873875

874-
const preferredModel = useModelsStore.getState().getEffectiveModel();
875-
if (preferredModel) {
876+
const modelToUse = model ?? useModelsStore.getState().getEffectiveModel();
877+
if (modelToUse) {
876878
await get().actions.setSessionConfigOptionByCategory(
877879
taskId,
878880
"model",
879-
preferredModel,
881+
modelToUse,
880882
);
881883
}
882884

@@ -986,6 +988,7 @@ const useStore = create<SessionStore>()(
986988
initialPrompt,
987989
executionMode,
988990
adapter,
991+
model,
989992
}) => {
990993
log.info("Connecting to task", { taskId: task.id });
991994

@@ -1110,6 +1113,7 @@ const useStore = create<SessionStore>()(
11101113
initialPrompt,
11111114
executionMode,
11121115
adapter,
1116+
model,
11131117
);
11141118
}
11151119
} catch (error) {

apps/twig/src/renderer/features/settings/stores/settingsStore.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ interface SettingsStore {
1515
lastUsedLocalWorkspaceMode: LocalWorkspaceMode;
1616
lastUsedWorkspaceMode: WorkspaceMode;
1717
lastUsedAdapter: AgentAdapter;
18+
lastUsedModel: string | null;
1819
desktopNotifications: boolean;
1920
dockBadgeNotifications: boolean;
2021
cursorGlow: boolean;
@@ -32,6 +33,7 @@ interface SettingsStore {
3233
setLastUsedLocalWorkspaceMode: (mode: LocalWorkspaceMode) => void;
3334
setLastUsedWorkspaceMode: (mode: WorkspaceMode) => void;
3435
setLastUsedAdapter: (adapter: AgentAdapter) => void;
36+
setLastUsedModel: (model: string) => void;
3537
setDesktopNotifications: (enabled: boolean) => void;
3638
setDockBadgeNotifications: (enabled: boolean) => void;
3739
setCursorGlow: (enabled: boolean) => void;
@@ -49,6 +51,7 @@ export const useSettingsStore = create<SettingsStore>()(
4951
lastUsedLocalWorkspaceMode: "worktree",
5052
lastUsedWorkspaceMode: "worktree",
5153
lastUsedAdapter: "claude",
54+
lastUsedModel: null,
5255
desktopNotifications: true,
5356
dockBadgeNotifications: true,
5457
completionSound: "none",
@@ -67,6 +70,7 @@ export const useSettingsStore = create<SettingsStore>()(
6770
set({ lastUsedLocalWorkspaceMode: mode }),
6871
setLastUsedWorkspaceMode: (mode) => set({ lastUsedWorkspaceMode: mode }),
6972
setLastUsedAdapter: (adapter) => set({ lastUsedAdapter: adapter }),
73+
setLastUsedModel: (model) => set({ lastUsedModel: model }),
7074
setDesktopNotifications: (enabled) =>
7175
set({ desktopNotifications: enabled }),
7276
setDockBadgeNotifications: (enabled) =>
@@ -89,6 +93,7 @@ export const useSettingsStore = create<SettingsStore>()(
8993
lastUsedLocalWorkspaceMode: state.lastUsedLocalWorkspaceMode,
9094
lastUsedWorkspaceMode: state.lastUsedWorkspaceMode,
9195
lastUsedAdapter: state.lastUsedAdapter,
96+
lastUsedModel: state.lastUsedModel,
9297
desktopNotifications: state.desktopNotifications,
9398
dockBadgeNotifications: state.dockBadgeNotifications,
9499
cursorGlow: state.cursorGlow,

apps/twig/src/renderer/features/task-detail/components/TaskInput.tsx

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { TorchGlow } from "@components/TorchGlow";
22
import { FolderPicker } from "@features/folder-picker/components/FolderPicker";
33
import type { MessageEditorHandle } from "@features/message-editor/components/MessageEditor";
4+
import { useModelsStore } from "@features/sessions/stores/modelsStore";
45
import type { AgentAdapter } from "@features/settings/stores/settingsStore";
56
import { useSettingsStore } from "@features/settings/stores/settingsStore";
67
import { useRepositoryIntegration } from "@hooks/useIntegrations";
@@ -13,6 +14,7 @@ import { useTaskCreation } from "../hooks/useTaskCreation";
1314
import { AdapterSelect } from "./AdapterSelect";
1415
import { SuggestedTasks } from "./SuggestedTasks";
1516
import { TaskInputEditor } from "./TaskInputEditor";
17+
import { TaskInputModelSelector } from "./TaskInputModelSelector";
1618
import { type WorkspaceMode, WorkspaceModeSelect } from "./WorkspaceModeSelect";
1719

1820
const DOT_FILL = "var(--gray-6)";
@@ -25,7 +27,10 @@ export function TaskInput() {
2527
setLastUsedLocalWorkspaceMode,
2628
lastUsedAdapter,
2729
setLastUsedAdapter,
30+
lastUsedModel,
31+
setLastUsedModel,
2832
} = useSettingsStore();
33+
const { getEffectiveModel } = useModelsStore();
2934

3035
const editorRef = useRef<MessageEditorHandle>(null);
3136
const containerRef = useRef<HTMLDivElement>(null);
@@ -36,13 +41,15 @@ export function TaskInput() {
3641
const selectedDirectory = lastUsedDirectory || "";
3742
const workspaceMode = lastUsedLocalWorkspaceMode || "worktree";
3843
const adapter = lastUsedAdapter;
44+
const selectedModel = lastUsedModel ?? getEffectiveModel();
3945

4046
const setSelectedDirectory = (path: string) =>
4147
setLastUsedDirectory(path || null);
4248
const setWorkspaceMode = (mode: WorkspaceMode) =>
4349
setLastUsedLocalWorkspaceMode(mode as "worktree" | "local");
4450
const setAdapter = (newAdapter: AgentAdapter) =>
4551
setLastUsedAdapter(newAdapter);
52+
const setSelectedModel = (model: string) => setLastUsedModel(model);
4653

4754
const { githubIntegration } = useRepositoryIntegration();
4855

@@ -56,6 +63,31 @@ export function TaskInput() {
5663
}
5764
}, [view.folderId, setLastUsedDirectory]);
5865

66+
// When adapter changes, validate that selected model is compatible
67+
useEffect(() => {
68+
const { groupedModels } = useModelsStore.getState();
69+
if (groupedModels.length === 0) return;
70+
71+
// Filter models by current adapter
72+
const compatibleModels =
73+
adapter === "claude"
74+
? groupedModels.filter((g) => g.provider === "Anthropic")
75+
: groupedModels.filter((g) => g.provider !== "Anthropic");
76+
77+
const allCompatibleModelIds = compatibleModels.flatMap((g) =>
78+
g.models.map((m) => m.modelId),
79+
);
80+
81+
// If current model is not compatible with adapter, select first available model
82+
if (!selectedModel || !allCompatibleModelIds.includes(selectedModel)) {
83+
// Get first available model for this adapter
84+
const firstCompatibleModel = compatibleModels[0]?.models[0]?.modelId;
85+
if (firstCompatibleModel) {
86+
setLastUsedModel(firstCompatibleModel);
87+
}
88+
}
89+
}, [adapter, selectedModel, setLastUsedModel]);
90+
5991
const effectiveWorkspaceMode = workspaceMode;
6092

6193
const { isCreatingTask, canSubmit, handleSubmit } = useTaskCreation({
@@ -66,6 +98,7 @@ export function TaskInput() {
6698
branch: null,
6799
editorIsEmpty,
68100
adapter,
101+
model: selectedModel,
69102
});
70103

71104
return (
@@ -146,6 +179,12 @@ export function TaskInput() {
146179
size="1"
147180
/>
148181
<AdapterSelect value={adapter} onChange={setAdapter} size="1" />
182+
<TaskInputModelSelector
183+
value={selectedModel}
184+
onChange={setSelectedModel}
185+
adapter={adapter}
186+
size="1"
187+
/>
149188
</Flex>
150189

151190
<TaskInputEditor
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { useModelsStore } from "@features/sessions/stores/modelsStore";
2+
import type { AgentAdapter } from "@features/settings/stores/settingsStore";
3+
import { ChevronDownIcon } from "@radix-ui/react-icons";
4+
import { Button, DropdownMenu, Flex, Text } from "@radix-ui/themes";
5+
import type { Responsive } from "@radix-ui/themes/dist/esm/props/prop-def.js";
6+
import { Fragment, useMemo } from "react";
7+
8+
interface TaskInputModelSelectorProps {
9+
value: string;
10+
onChange: (modelId: string) => void;
11+
adapter: AgentAdapter;
12+
size?: Responsive<"1" | "2">;
13+
}
14+
15+
function filterModelsByAdapter(
16+
groupedModels: Array<{
17+
provider: string;
18+
models: Array<{ modelId: string; name: string }>;
19+
}>,
20+
adapter: AgentAdapter,
21+
) {
22+
if (adapter === "claude") {
23+
// Claude adapter: show only Anthropic models
24+
return groupedModels.filter((group) => group.provider === "Anthropic");
25+
}
26+
// Codex adapter: show OpenAI and other non-Anthropic models
27+
return groupedModels.filter((group) => group.provider !== "Anthropic");
28+
}
29+
30+
export function TaskInputModelSelector({
31+
value,
32+
onChange,
33+
adapter,
34+
size = "1",
35+
}: TaskInputModelSelectorProps) {
36+
const { groupedModels } = useModelsStore();
37+
38+
const filteredGroupedModels = useMemo(
39+
() => filterModelsByAdapter(groupedModels, adapter),
40+
[groupedModels, adapter],
41+
);
42+
43+
const filteredModels = useMemo(
44+
() => filteredGroupedModels.flatMap((group) => group.models),
45+
[filteredGroupedModels],
46+
);
47+
48+
if (filteredModels.length === 0) {
49+
return null;
50+
}
51+
52+
const currentModel = filteredModels.find((m) => m.modelId === value);
53+
const displayName = currentModel?.name ?? value;
54+
55+
return (
56+
<DropdownMenu.Root>
57+
<DropdownMenu.Trigger>
58+
<Button color="gray" variant="outline" size={size}>
59+
<Flex justify="between" align="center" gap="2">
60+
<Text
61+
size={size}
62+
style={{ fontFamily: "var(--font-mono)", minWidth: 0 }}
63+
>
64+
{displayName}
65+
</Text>
66+
<ChevronDownIcon style={{ flexShrink: 0 }} />
67+
</Flex>
68+
</Button>
69+
</DropdownMenu.Trigger>
70+
71+
<DropdownMenu.Content align="start" size="1">
72+
{filteredGroupedModels.map((group, groupIndex) => (
73+
<Fragment key={group.provider}>
74+
{groupIndex > 0 && <DropdownMenu.Separator />}
75+
<DropdownMenu.Label>{group.provider}</DropdownMenu.Label>
76+
{group.models.map((model) => (
77+
<DropdownMenu.Item
78+
key={model.modelId}
79+
onSelect={() => onChange(model.modelId)}
80+
>
81+
<Text size="1">{model.name}</Text>
82+
</DropdownMenu.Item>
83+
))}
84+
</Fragment>
85+
))}
86+
</DropdownMenu.Content>
87+
</DropdownMenu.Root>
88+
);
89+
}

apps/twig/src/renderer/features/task-detail/hooks/useTaskCreation.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ interface UseTaskCreationOptions {
2727
editorIsEmpty: boolean;
2828
executionMode?: string;
2929
adapter?: "claude" | "codex";
30+
model?: string;
3031
}
3132

3233
interface UseTaskCreationReturn {
@@ -82,6 +83,7 @@ function prepareTaskInput(
8283
branch?: string | null;
8384
executionMode?: string;
8485
adapter?: "claude" | "codex";
86+
model?: string;
8587
},
8688
): TaskCreationInput {
8789
return {
@@ -94,6 +96,7 @@ function prepareTaskInput(
9496
branch: options.branch,
9597
executionMode: options.executionMode,
9698
adapter: options.adapter,
99+
model: options.model,
97100
};
98101
}
99102

@@ -119,6 +122,7 @@ export function useTaskCreation({
119122
editorIsEmpty,
120123
executionMode,
121124
adapter,
125+
model,
122126
}: UseTaskCreationOptions): UseTaskCreationReturn {
123127
const [isCreatingTask, setIsCreatingTask] = useState(false);
124128
const { navigateToTask } = useNavigationStore();
@@ -158,6 +162,7 @@ export function useTaskCreation({
158162
branch,
159163
executionMode,
160164
adapter,
165+
model,
161166
});
162167

163168
const taskService = get<TaskService>(RENDERER_TOKENS.TaskService);
@@ -201,6 +206,7 @@ export function useTaskCreation({
201206
branch,
202207
executionMode,
203208
adapter,
209+
model,
204210
invalidateTasks,
205211
navigateToTask,
206212
]);

apps/twig/src/renderer/sagas/task/task-creation.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export interface TaskCreationInput {
3232
githubIntegrationId?: number;
3333
executionMode?: string;
3434
adapter?: "claude" | "codex";
35+
model?: string;
3536
}
3637

3738
export interface TaskCreationOutput {
@@ -202,6 +203,7 @@ export class TaskCreationSaga extends Saga<
202203
initialPrompt,
203204
executionMode: input.executionMode,
204205
adapter: input.adapter,
206+
model: input.model,
205207
});
206208
}
207209
return { taskId: task.id };

packages/agent/src/gateway-models.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export interface FetchGatewayModelsOptions {
1515
gatewayUrl: string;
1616
}
1717

18-
export const DEFAULT_GATEWAY_MODEL = "claude-opus-4-5";
18+
export const DEFAULT_GATEWAY_MODEL = "claude-opus-4-6";
1919

2020
export const BLOCKED_MODELS = new Set(["gpt-5-mini", "openai/gpt-5-mini"]);
2121

0 commit comments

Comments
 (0)