/** * Project Models section (U9 / KTD-10). * * Project-scoped model configuration that survives the workflow hard-move: token * cap, the project DEFAULT model lane, model presets (with the inline editor and * size-based auto-selection), and the title/commit summarization toggles. The * per-phase execution/planning/validator lanes and the title-summarizer lane * moved to the workflow (U4) and render as a redirect stub. The model-lane * helpers, preset draft state/handlers, available-model list, favorites, and the * confirm dialog all live in the shell (they share state with the save flow and * the global model lanes) and are relayed through a `models` prop bag — mirroring * the Authentication/Remote section conventions. Keys, lane labels, and * conditional rendering are preserved verbatim from the original inline JSX. */ import type { ReactNode } from "react"; import { useTranslation } from "react-i18next"; import type { ModelPreset, Settings } from "@fusion/core"; import type { ModelInfo } from "../../../api"; import { CustomModelDropdown } from "../../CustomModelDropdown"; import { applyPresetToSelection } from "../../../utils/modelPresets"; import { MovedSettingsStub } from "./MovedSettingsStub"; import type { ModelLane, SectionBaseProps, SettingsFormState } from "./context"; type LaneStatus = "inherited" | "overridden"; export interface ProjectModelsSectionModelProps { modelLanes: ModelLane[]; getLaneStatus: (lane: ModelLane) => LaneStatus; getLaneValue: (lane: ModelLane) => string; updateLaneValue: (lane: ModelLane, value: string) => void; resetLaneValue: (lane: ModelLane) => void; availableModels: ModelInfo[]; modelsLoading: boolean; favoriteProviders: string[]; favoriteModels: string[]; onToggleFavorite: (provider: string) => void; onToggleModelFavorite: (modelId: string) => void; editingPresetId: string | null; setEditingPresetId: (id: string | null) => void; presetDraft: ModelPreset | null; setPresetDraft: (updater: ModelPreset | null | ((prev: ModelPreset | null) => ModelPreset | null)) => void; onSavePresetDraft: () => void; confirmDelete: (options: { title: string; message: string; danger?: boolean }) => Promise; } export interface ProjectModelsSectionProps extends SectionBaseProps { scopeBanner: ReactNode; models: ProjectModelsSectionModelProps; onOpenWorkflowSettings?: () => void; } export function ProjectModelsSection({ scopeBanner, form, setForm, models, onOpenWorkflowSettings }: ProjectModelsSectionProps) { const { t } = useTranslation("app"); const { modelLanes, getLaneStatus, getLaneValue, updateLaneValue, resetLaneValue, availableModels, modelsLoading, favoriteProviders, favoriteModels, onToggleFavorite, onToggleModelFavorite, editingPresetId, setEditingPresetId, presetDraft, setPresetDraft, onSavePresetDraft, confirmDelete, } = models; const presets = form.modelPresets || []; const presetOptions = presets.map((preset) => ({ id: preset.id, name: preset.name })); const inUsePresetIds = new Set(Object.values(form.defaultPresetBySize || {}).filter(Boolean)); // Only the project DEFAULT model lane survives in this modal. The // per-phase execution/planning/validator lanes, their fallbacks, and the // title-summarizer lane were hard-moved (U4) onto the workflow settings // mechanism — they are no longer project settings keys and must never be // renderable or savable here (redirect stub below). const projectModelLanes = modelLanes.filter((lane) => lane.laneId === "default"); const getProjectLaneLabel = (lane: ModelLane) => lane.laneId === "default" ? "Project Default Model" : lane.label; const getProjectLaneHelperText = (lane: ModelLane) => lane.laneId === "default" ? "Project-wide default AI model used when no more specific task or project lane override is set." : lane.helperText; return ( <> {scopeBanner} {/* --- Token Cap --- */}

Token Cap

{ const val = e.target.value; setForm((f) => ({ ...f, tokenCap: val ? parseInt(val, 10) : null } as SettingsFormState)); }} /> {form.tokenCap != null && ( )}
Automatically compact context when approaching this token count. Leave empty for no cap (compact only on overflow errors). Set a number to proactively compact when reaching this token count.
{/* --- Project Model Lanes --- */}

Model Lanes

Override global model settings at the project level. Each lane controls a specific AI usage context. Unset lanes inherit from the corresponding global lane. The Project Default Model is the fallback for this project when a more specific lane is unset.

{modelsLoading ? (
Loading available models…
) : availableModels.length === 0 ? (
No models available. Configure authentication first.
) : ( <> {projectModelLanes.map((lane) => { const status = getLaneStatus(lane); const value = getLaneValue(lane); const isOverridden = status === "overridden"; const laneLabel = getProjectLaneLabel(lane); return (
{isOverridden ? "Override (Project)" : "Inherited (Global)"}
updateLaneValue(lane, val)} placeholder={lane.laneId === "default" ? "Use global default" : "Use global"} favoriteProviders={favoriteProviders} onToggleFavorite={onToggleFavorite} favoriteModels={favoriteModels} onToggleModelFavorite={onToggleModelFavorite} />
{isOverridden && ( )}
{getProjectLaneHelperText(lane)} Falls back to: {lane.fallbackOrder}.
); })} )} {/* --- Per-phase model lanes (MOVED to workflow settings) --- */}

Per-phase model lanes

{/* --- Model Presets --- */}

Model Presets

{presets.length === 0 ? (
No presets configured yet.
) : (
{presets.map((preset) => { const selection = applyPresetToSelection(preset); const summary = `${selection.executorValue || "default"} / ${selection.validatorValue || "default"}`; return (
{preset.name} {summary}
); })}
)} {!presetDraft ? (
) : null}
{presetDraft ? (
{ const name = e.target.value; setPresetDraft((current) => current ? { ...current, name } : current); }} />
{availableModels.length === 0 ? ( No models available. Configure authentication first. ) : ( <>
{ if (!val) { setPresetDraft((current) => current ? { ...current, executorProvider: undefined, executorModelId: undefined } : current); return; } const slashIdx = val.indexOf("/"); setPresetDraft((current) => current ? { ...current, executorProvider: val.slice(0, slashIdx), executorModelId: val.slice(slashIdx + 1), } : current); }} placeholder="Use default" favoriteProviders={favoriteProviders} onToggleFavorite={onToggleFavorite} favoriteModels={favoriteModels} onToggleModelFavorite={onToggleModelFavorite} />
{ if (!val) { setPresetDraft((current) => current ? { ...current, validatorProvider: undefined, validatorModelId: undefined } : current); return; } const slashIdx = val.indexOf("/"); setPresetDraft((current) => current ? { ...current, validatorProvider: val.slice(0, slashIdx), validatorModelId: val.slice(slashIdx + 1), } : current); }} placeholder="Use default" favoriteProviders={favoriteProviders} onToggleFavorite={onToggleFavorite} favoriteModels={favoriteModels} onToggleModelFavorite={onToggleModelFavorite} />
)}
) : null}
{form.autoSelectModelPreset ? (
{(["S", "M", "L"] as const).map((sizeKey) => (
))}
) : null} {/* --- AI Title and Git Commit Message Summarization --- */}

AI Title and Git Commit Message Summarization

Configures the model used for two short-summary jobs: auto-generating task titles from long descriptions, and generating merge commit summaries from step commits and diff stats.

When enabled, tasks created without a title but with descriptions over 200 characters will automatically get an AI-generated title (max 60 characters). The same model is also used to generate fallback merge commit message bodies when the branch's commit log is empty (e.g. squash merges with no unique commits), and GitHub tracking issue titles when a tracked task has no title yet.
When enabled, merge commit messages include an AI-generated subject plus body summary (narrative + bullets + diff-stat) instead of just listing step commit subjects. Uses the title summarization model.
{(form.autoSummarizeTitles || form.useAiMergeCommitSummary || form.githubTrackingEnabledByDefault || false) && (

{t( "settings.movedStub.summarizerModelInline", "The model used for summarization now lives on the workflow (title summarizer lane). Open workflow settings to choose it.", )}

)} ); } export default ProjectModelsSection;