Files
automaker/apps/ui/src/components/shared/use-model-override.ts
gsxdsm e7504b247f Add quick-add feature with improved workflows (#802)
* Changes from feature/quick-add

* feat: Clarify system prompt and improve error handling across services. Address PR Feedback

* feat: Improve PR description parsing and refactor event handling

* feat: Add context options to pipeline orchestrator initialization

* fix: Deduplicate React and handle CJS interop for use-sync-external-store

Resolve "Cannot read properties of null (reading 'useState')" errors by
deduplicating React/react-dom and ensuring use-sync-external-store is
bundled together with React to prevent CJS packages from resolving to
different React instances.
2026-02-22 20:48:09 -08:00

110 lines
3.4 KiB
TypeScript

import { useState, useCallback, useMemo } from 'react';
import { useAppStore } from '@/store/app-store';
import type { ModelId, PhaseModelKey, PhaseModelEntry } from '@automaker/types';
import { DEFAULT_PHASE_MODELS } from '@automaker/types';
export interface UseModelOverrideOptions {
/** Which phase this override is for */
phase: PhaseModelKey;
/** Initial override value (optional) */
initialOverride?: PhaseModelEntry | null;
}
export interface UseModelOverrideResult {
/** The effective model entry (override or global default) */
effectiveModelEntry: PhaseModelEntry;
/** The effective model string (for backward compatibility with APIs that only accept strings) */
effectiveModel: ModelId;
/** Whether the model is currently overridden */
isOverridden: boolean;
/** Set a model override */
setOverride: (entry: PhaseModelEntry | null) => void;
/** Clear the override and use global default */
clearOverride: () => void;
/** The global default for this phase */
globalDefault: PhaseModelEntry;
/** The current override value (null if not overridden) */
override: PhaseModelEntry | null;
}
/**
* Normalize PhaseModelEntry or string to PhaseModelEntry
* Handles undefined/null gracefully (e.g., when phaseModels from server settings
* is missing a recently-added phase key)
*/
function normalizeEntry(entry: PhaseModelEntry | string | undefined | null): PhaseModelEntry {
if (!entry) {
return { model: 'claude-sonnet' as ModelId };
}
if (typeof entry === 'string') {
return { model: entry as ModelId };
}
return entry;
}
/**
* Hook for managing model overrides per phase
*
* Provides a simple way to allow users to override the global phase model
* for a specific run or context. Now supports PhaseModelEntry with thinking levels.
*
* @example
* ```tsx
* function EnhanceDialog() {
* const { effectiveModelEntry, isOverridden, setOverride, clearOverride } = useModelOverride({
* phase: 'enhancementModel',
* });
*
* return (
* <ModelOverrideTrigger
* currentModelEntry={effectiveModelEntry}
* onModelChange={setOverride}
* phase="enhancementModel"
* isOverridden={isOverridden}
* />
* );
* }
* ```
*/
export function useModelOverride({
phase,
initialOverride = null,
}: UseModelOverrideOptions): UseModelOverrideResult {
const { phaseModels } = useAppStore();
const [override, setOverrideState] = useState<PhaseModelEntry | null>(
initialOverride ? normalizeEntry(initialOverride) : null
);
// Normalize global default to PhaseModelEntry, with fallback to DEFAULT_PHASE_MODELS
// This handles cases where settings haven't been migrated to include new phase models
const globalDefault = normalizeEntry(phaseModels[phase] ?? DEFAULT_PHASE_MODELS[phase]);
const effectiveModelEntry = useMemo(() => {
return override ?? globalDefault;
}, [override, globalDefault]);
const effectiveModel = useMemo(() => {
return effectiveModelEntry.model;
}, [effectiveModelEntry]);
const isOverridden = override !== null;
const setOverride = useCallback((entry: PhaseModelEntry | null) => {
setOverrideState(entry ? normalizeEntry(entry) : null);
}, []);
const clearOverride = useCallback(() => {
setOverrideState(null);
}, []);
return {
effectiveModelEntry,
effectiveModel,
isOverridden,
setOverride,
clearOverride,
globalDefault,
override,
};
}