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
10 changes: 7 additions & 3 deletions apps/twig/src/renderer/features/sessions/stores/sessionStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ interface SessionActions {
initialPrompt?: ContentBlock[];
executionMode?: string;
adapter?: "claude" | "codex";
model?: string;
}) => Promise<void>;
disconnectFromTask: (taskId: string) => Promise<void>;
sendPrompt: (
Expand Down Expand Up @@ -819,6 +820,7 @@ const useStore = create<SessionStore>()(
initialPrompt?: ContentBlock[],
executionMode?: string,
adapter?: "claude" | "codex",
model?: string,
) => {
if (!auth.client) {
throw new Error(
Expand Down Expand Up @@ -871,12 +873,12 @@ const useStore = create<SessionStore>()(
);
}

const preferredModel = useModelsStore.getState().getEffectiveModel();
if (preferredModel) {
const modelToUse = model ?? useModelsStore.getState().getEffectiveModel();
if (modelToUse) {
await get().actions.setSessionConfigOptionByCategory(
taskId,
"model",
preferredModel,
modelToUse,
);
}

Expand Down Expand Up @@ -986,6 +988,7 @@ const useStore = create<SessionStore>()(
initialPrompt,
executionMode,
adapter,
model,
}) => {
log.info("Connecting to task", { taskId: task.id });

Expand Down Expand Up @@ -1110,6 +1113,7 @@ const useStore = create<SessionStore>()(
initialPrompt,
executionMode,
adapter,
model,
);
}
} catch (error) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ interface SettingsStore {
lastUsedLocalWorkspaceMode: LocalWorkspaceMode;
lastUsedWorkspaceMode: WorkspaceMode;
lastUsedAdapter: AgentAdapter;
lastUsedModel: string | null;
desktopNotifications: boolean;
dockBadgeNotifications: boolean;
cursorGlow: boolean;
Expand All @@ -32,6 +33,7 @@ interface SettingsStore {
setLastUsedLocalWorkspaceMode: (mode: LocalWorkspaceMode) => void;
setLastUsedWorkspaceMode: (mode: WorkspaceMode) => void;
setLastUsedAdapter: (adapter: AgentAdapter) => void;
setLastUsedModel: (model: string) => void;
setDesktopNotifications: (enabled: boolean) => void;
setDockBadgeNotifications: (enabled: boolean) => void;
setCursorGlow: (enabled: boolean) => void;
Expand All @@ -49,6 +51,7 @@ export const useSettingsStore = create<SettingsStore>()(
lastUsedLocalWorkspaceMode: "worktree",
lastUsedWorkspaceMode: "worktree",
lastUsedAdapter: "claude",
lastUsedModel: null,
desktopNotifications: true,
dockBadgeNotifications: true,
completionSound: "none",
Expand All @@ -67,6 +70,7 @@ export const useSettingsStore = create<SettingsStore>()(
set({ lastUsedLocalWorkspaceMode: mode }),
setLastUsedWorkspaceMode: (mode) => set({ lastUsedWorkspaceMode: mode }),
setLastUsedAdapter: (adapter) => set({ lastUsedAdapter: adapter }),
setLastUsedModel: (model) => set({ lastUsedModel: model }),
setDesktopNotifications: (enabled) =>
set({ desktopNotifications: enabled }),
setDockBadgeNotifications: (enabled) =>
Expand All @@ -89,6 +93,7 @@ export const useSettingsStore = create<SettingsStore>()(
lastUsedLocalWorkspaceMode: state.lastUsedLocalWorkspaceMode,
lastUsedWorkspaceMode: state.lastUsedWorkspaceMode,
lastUsedAdapter: state.lastUsedAdapter,
lastUsedModel: state.lastUsedModel,
desktopNotifications: state.desktopNotifications,
dockBadgeNotifications: state.dockBadgeNotifications,
cursorGlow: state.cursorGlow,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { TorchGlow } from "@components/TorchGlow";
import { FolderPicker } from "@features/folder-picker/components/FolderPicker";
import type { MessageEditorHandle } from "@features/message-editor/components/MessageEditor";
import { useModelsStore } from "@features/sessions/stores/modelsStore";
import type { AgentAdapter } from "@features/settings/stores/settingsStore";
import { useSettingsStore } from "@features/settings/stores/settingsStore";
import { useRepositoryIntegration } from "@hooks/useIntegrations";
Expand All @@ -13,6 +14,7 @@ import { useTaskCreation } from "../hooks/useTaskCreation";
import { AdapterSelect } from "./AdapterSelect";
import { SuggestedTasks } from "./SuggestedTasks";
import { TaskInputEditor } from "./TaskInputEditor";
import { TaskInputModelSelector } from "./TaskInputModelSelector";
import { type WorkspaceMode, WorkspaceModeSelect } from "./WorkspaceModeSelect";

const DOT_FILL = "var(--gray-6)";
Expand All @@ -25,7 +27,10 @@ export function TaskInput() {
setLastUsedLocalWorkspaceMode,
lastUsedAdapter,
setLastUsedAdapter,
lastUsedModel,
setLastUsedModel,
} = useSettingsStore();
const { getEffectiveModel } = useModelsStore();

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

const setSelectedDirectory = (path: string) =>
setLastUsedDirectory(path || null);
const setWorkspaceMode = (mode: WorkspaceMode) =>
setLastUsedLocalWorkspaceMode(mode as "worktree" | "local");
const setAdapter = (newAdapter: AgentAdapter) =>
setLastUsedAdapter(newAdapter);
const setSelectedModel = (model: string) => setLastUsedModel(model);

const { githubIntegration } = useRepositoryIntegration();

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

// When adapter changes, validate that selected model is compatible
useEffect(() => {
const { groupedModels } = useModelsStore.getState();
if (groupedModels.length === 0) return;

// Filter models by current adapter
const compatibleModels =
adapter === "claude"
? groupedModels.filter((g) => g.provider === "Anthropic")
: groupedModels.filter((g) => g.provider !== "Anthropic");

const allCompatibleModelIds = compatibleModels.flatMap((g) =>
g.models.map((m) => m.modelId),
);

// If current model is not compatible with adapter, select first available model
if (!selectedModel || !allCompatibleModelIds.includes(selectedModel)) {
// Get first available model for this adapter
const firstCompatibleModel = compatibleModels[0]?.models[0]?.modelId;
if (firstCompatibleModel) {
setLastUsedModel(firstCompatibleModel);
}
}
}, [adapter, selectedModel, setLastUsedModel]);

const effectiveWorkspaceMode = workspaceMode;

const { isCreatingTask, canSubmit, handleSubmit } = useTaskCreation({
Expand All @@ -66,6 +98,7 @@ export function TaskInput() {
branch: null,
editorIsEmpty,
adapter,
model: selectedModel,
});

return (
Expand Down Expand Up @@ -146,6 +179,12 @@ export function TaskInput() {
size="1"
/>
<AdapterSelect value={adapter} onChange={setAdapter} size="1" />
<TaskInputModelSelector
value={selectedModel}
onChange={setSelectedModel}
adapter={adapter}
size="1"
/>
</Flex>

<TaskInputEditor
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { useModelsStore } from "@features/sessions/stores/modelsStore";
import type { AgentAdapter } from "@features/settings/stores/settingsStore";
import { ChevronDownIcon } from "@radix-ui/react-icons";
import { Button, DropdownMenu, Flex, Text } from "@radix-ui/themes";
import type { Responsive } from "@radix-ui/themes/dist/esm/props/prop-def.js";
import { Fragment, useMemo } from "react";

interface TaskInputModelSelectorProps {
value: string;
onChange: (modelId: string) => void;
adapter: AgentAdapter;
size?: Responsive<"1" | "2">;
}

function filterModelsByAdapter(
groupedModels: Array<{
provider: string;
models: Array<{ modelId: string; name: string }>;
}>,
adapter: AgentAdapter,
) {
if (adapter === "claude") {
// Claude adapter: show only Anthropic models
return groupedModels.filter((group) => group.provider === "Anthropic");
}
// Codex adapter: show OpenAI and other non-Anthropic models
return groupedModels.filter((group) => group.provider !== "Anthropic");
}

export function TaskInputModelSelector({
value,
onChange,
adapter,
size = "1",
}: TaskInputModelSelectorProps) {
const { groupedModels } = useModelsStore();

const filteredGroupedModels = useMemo(
() => filterModelsByAdapter(groupedModels, adapter),
[groupedModels, adapter],
);

const filteredModels = useMemo(
() => filteredGroupedModels.flatMap((group) => group.models),
[filteredGroupedModels],
);

if (filteredModels.length === 0) {
return null;
}

const currentModel = filteredModels.find((m) => m.modelId === value);
const displayName = currentModel?.name ?? value;

return (
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<Button color="gray" variant="outline" size={size}>
<Flex justify="between" align="center" gap="2">
<Text
size={size}
style={{ fontFamily: "var(--font-mono)", minWidth: 0 }}
>
{displayName}
</Text>
<ChevronDownIcon style={{ flexShrink: 0 }} />
</Flex>
</Button>
</DropdownMenu.Trigger>

<DropdownMenu.Content align="start" size="1">
{filteredGroupedModels.map((group, groupIndex) => (
<Fragment key={group.provider}>
{groupIndex > 0 && <DropdownMenu.Separator />}
<DropdownMenu.Label>{group.provider}</DropdownMenu.Label>
{group.models.map((model) => (
<DropdownMenu.Item
key={model.modelId}
onSelect={() => onChange(model.modelId)}
>
<Text size="1">{model.name}</Text>
</DropdownMenu.Item>
))}
</Fragment>
))}
</DropdownMenu.Content>
</DropdownMenu.Root>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ interface UseTaskCreationOptions {
editorIsEmpty: boolean;
executionMode?: string;
adapter?: "claude" | "codex";
model?: string;
}

interface UseTaskCreationReturn {
Expand Down Expand Up @@ -82,6 +83,7 @@ function prepareTaskInput(
branch?: string | null;
executionMode?: string;
adapter?: "claude" | "codex";
model?: string;
},
): TaskCreationInput {
return {
Expand All @@ -94,6 +96,7 @@ function prepareTaskInput(
branch: options.branch,
executionMode: options.executionMode,
adapter: options.adapter,
model: options.model,
};
}

Expand All @@ -119,6 +122,7 @@ export function useTaskCreation({
editorIsEmpty,
executionMode,
adapter,
model,
}: UseTaskCreationOptions): UseTaskCreationReturn {
const [isCreatingTask, setIsCreatingTask] = useState(false);
const { navigateToTask } = useNavigationStore();
Expand Down Expand Up @@ -158,6 +162,7 @@ export function useTaskCreation({
branch,
executionMode,
adapter,
model,
});

const taskService = get<TaskService>(RENDERER_TOKENS.TaskService);
Expand Down Expand Up @@ -201,6 +206,7 @@ export function useTaskCreation({
branch,
executionMode,
adapter,
model,
invalidateTasks,
navigateToTask,
]);
Expand Down
2 changes: 2 additions & 0 deletions apps/twig/src/renderer/sagas/task/task-creation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export interface TaskCreationInput {
githubIntegrationId?: number;
executionMode?: string;
adapter?: "claude" | "codex";
model?: string;
}

export interface TaskCreationOutput {
Expand Down Expand Up @@ -202,6 +203,7 @@ export class TaskCreationSaga extends Saga<
initialPrompt,
executionMode: input.executionMode,
adapter: input.adapter,
model: input.model,
});
}
return { taskId: task.id };
Expand Down
2 changes: 1 addition & 1 deletion packages/agent/src/gateway-models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export interface FetchGatewayModelsOptions {
gatewayUrl: string;
}

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

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

Expand Down
Loading