Unify utility model settings and align git generation (#486)
* refactor(api): extend git generation payload types * refactor(settings): add git provider model fields * feat(config): persist git provider model defaults * feat(git-api): send provider and model ids * fix(git-api): forward generation options in runtime * feat(git-view): use configured model for commit generation * feat(git-view): pass configured model for PR generation * feat(vscode): forward model selection in git bridge payload * feat(vscode): align PR generation with session model flow * feat(web): resolve and generate git text with provider model * refactor(settings): unify utility model picker across providers * chore(settings): rename sidebar item to utility model * fix(git-model): validate and auto-heal stale utility selections --------- Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
This commit is contained in:
committed by
GitHub
co-authored by
Bohdan Triapitsyn
parent
8647e0c1a4
commit
7a11867a19
@@ -18,6 +18,8 @@ const MODELS_DEV_PROXY_URL = "/api/openchamber/models-metadata";
|
||||
|
||||
const FALLBACK_PROVIDER_ID = "opencode";
|
||||
const FALLBACK_MODEL_ID = "big-pickle";
|
||||
const GIT_UTILITY_PROVIDER_ID = "zen";
|
||||
const GIT_UTILITY_PREFERRED_MODEL_ID = "big-pickle";
|
||||
|
||||
interface OpenChamberDefaults {
|
||||
defaultModel?: string;
|
||||
@@ -26,6 +28,8 @@ interface OpenChamberDefaults {
|
||||
autoCreateWorktree?: boolean;
|
||||
gitmojiEnabled?: boolean;
|
||||
zenModel?: string;
|
||||
gitProviderId?: string;
|
||||
gitModelId?: string;
|
||||
}
|
||||
|
||||
const fetchOpenChamberDefaults = async (): Promise<OpenChamberDefaults> => {
|
||||
@@ -42,6 +46,8 @@ const fetchOpenChamberDefaults = async (): Promise<OpenChamberDefaults> => {
|
||||
const defaultAgent = typeof data?.defaultAgent === 'string' ? data.defaultAgent.trim() : '';
|
||||
const gitmojiEnabled = typeof data?.gitmojiEnabled === 'boolean' ? data.gitmojiEnabled : undefined;
|
||||
const zenModel = typeof data?.zenModel === 'string' ? data.zenModel.trim() : '';
|
||||
const gitProviderId = typeof data?.gitProviderId === 'string' ? data.gitProviderId.trim() : '';
|
||||
const gitModelId = typeof data?.gitModelId === 'string' ? data.gitModelId.trim() : '';
|
||||
|
||||
return {
|
||||
defaultModel: defaultModel.length > 0 ? defaultModel : undefined,
|
||||
@@ -50,6 +56,8 @@ const fetchOpenChamberDefaults = async (): Promise<OpenChamberDefaults> => {
|
||||
autoCreateWorktree: typeof data?.autoCreateWorktree === 'boolean' ? data.autoCreateWorktree : undefined,
|
||||
gitmojiEnabled,
|
||||
zenModel: zenModel.length > 0 ? zenModel : undefined,
|
||||
gitProviderId: gitProviderId.length > 0 ? gitProviderId : undefined,
|
||||
gitModelId: gitModelId.length > 0 ? gitModelId : undefined,
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
@@ -71,6 +79,8 @@ const fetchOpenChamberDefaults = async (): Promise<OpenChamberDefaults> => {
|
||||
const defaultAgent = typeof data?.defaultAgent === 'string' ? data.defaultAgent.trim() : '';
|
||||
const gitmojiEnabled = typeof data?.gitmojiEnabled === 'boolean' ? data.gitmojiEnabled : undefined;
|
||||
const zenModel = typeof data?.zenModel === 'string' ? data.zenModel.trim() : '';
|
||||
const gitProviderId = typeof data?.gitProviderId === 'string' ? data.gitProviderId.trim() : '';
|
||||
const gitModelId = typeof data?.gitModelId === 'string' ? data.gitModelId.trim() : '';
|
||||
|
||||
return {
|
||||
defaultModel: defaultModel.length > 0 ? defaultModel : undefined,
|
||||
@@ -79,6 +89,8 @@ const fetchOpenChamberDefaults = async (): Promise<OpenChamberDefaults> => {
|
||||
autoCreateWorktree: typeof data?.autoCreateWorktree === 'boolean' ? data.autoCreateWorktree : undefined,
|
||||
gitmojiEnabled,
|
||||
zenModel: zenModel.length > 0 ? zenModel : undefined,
|
||||
gitProviderId: gitProviderId.length > 0 ? gitProviderId : undefined,
|
||||
gitModelId: gitModelId.length > 0 ? gitModelId : undefined,
|
||||
};
|
||||
} catch {
|
||||
return {};
|
||||
@@ -103,6 +115,80 @@ const isPrimaryMode = (mode?: string) => mode === "primary" || mode === "all" ||
|
||||
type ProviderModel = Provider["models"][string];
|
||||
type ProviderWithModelList = Omit<Provider, "models"> & { models: ProviderModel[] };
|
||||
|
||||
type GitModelSelection = { providerId: string; modelId: string };
|
||||
|
||||
const normalizeOptionalString = (value: unknown): string | undefined => {
|
||||
if (typeof value !== "string") {
|
||||
return undefined;
|
||||
}
|
||||
const trimmed = value.trim();
|
||||
return trimmed.length > 0 ? trimmed : undefined;
|
||||
};
|
||||
|
||||
const hasProviderModel = (
|
||||
providers: ProviderWithModelList[],
|
||||
providerId: string,
|
||||
modelId: string
|
||||
): boolean => {
|
||||
const provider = providers.find((item) => item.id === providerId);
|
||||
if (!provider) {
|
||||
return false;
|
||||
}
|
||||
return provider.models.some((model) => model.id === modelId);
|
||||
};
|
||||
|
||||
const resolveGitGenerationModelSelection = ({
|
||||
providers,
|
||||
settingsGitProviderId,
|
||||
settingsGitModelId,
|
||||
settingsZenModel,
|
||||
}: {
|
||||
providers: ProviderWithModelList[];
|
||||
settingsGitProviderId?: string;
|
||||
settingsGitModelId?: string;
|
||||
settingsZenModel?: string;
|
||||
}): GitModelSelection | null => {
|
||||
const gitProviderId = normalizeOptionalString(settingsGitProviderId);
|
||||
const gitModelId = normalizeOptionalString(settingsGitModelId);
|
||||
const zenModel = normalizeOptionalString(settingsZenModel);
|
||||
|
||||
if (!Array.isArray(providers) || providers.length === 0) {
|
||||
if (zenModel) {
|
||||
return { providerId: GIT_UTILITY_PROVIDER_ID, modelId: zenModel };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
if (gitProviderId && gitModelId && hasProviderModel(providers, gitProviderId, gitModelId)) {
|
||||
return { providerId: gitProviderId, modelId: gitModelId };
|
||||
}
|
||||
|
||||
if (zenModel && hasProviderModel(providers, GIT_UTILITY_PROVIDER_ID, zenModel)) {
|
||||
return { providerId: GIT_UTILITY_PROVIDER_ID, modelId: zenModel };
|
||||
}
|
||||
|
||||
if (hasProviderModel(providers, GIT_UTILITY_PROVIDER_ID, GIT_UTILITY_PREFERRED_MODEL_ID)) {
|
||||
return { providerId: GIT_UTILITY_PROVIDER_ID, modelId: GIT_UTILITY_PREFERRED_MODEL_ID };
|
||||
}
|
||||
|
||||
const zenProvider = providers.find((provider) => provider.id === GIT_UTILITY_PROVIDER_ID);
|
||||
if (zenProvider?.models.length) {
|
||||
const randomIndex = Math.floor(Math.random() * zenProvider.models.length);
|
||||
const randomModelId = normalizeOptionalString(zenProvider.models[randomIndex]?.id);
|
||||
if (randomModelId) {
|
||||
return { providerId: GIT_UTILITY_PROVIDER_ID, modelId: randomModelId };
|
||||
}
|
||||
}
|
||||
|
||||
const firstProvider = providers.find((provider) => provider.models.length > 0);
|
||||
const firstModelId = normalizeOptionalString(firstProvider?.models[0]?.id);
|
||||
if (firstProvider?.id && firstModelId) {
|
||||
return { providerId: firstProvider.id, modelId: firstModelId };
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
interface ModelsDevModelEntry {
|
||||
id?: string;
|
||||
name?: string;
|
||||
@@ -372,6 +458,8 @@ interface ConfigStore {
|
||||
settingsAutoCreateWorktree: boolean;
|
||||
settingsGitmojiEnabled: boolean;
|
||||
settingsZenModel: string | undefined;
|
||||
settingsGitProviderId: string | undefined;
|
||||
settingsGitModelId: string | undefined;
|
||||
// Voice provider preference ('browser', 'openai', or 'say' for macOS)
|
||||
voiceProvider: 'browser' | 'openai' | 'say';
|
||||
setVoiceProvider: (provider: 'browser' | 'openai' | 'say') => void;
|
||||
@@ -421,6 +509,9 @@ interface ConfigStore {
|
||||
setSettingsAutoCreateWorktree: (enabled: boolean) => void;
|
||||
setSettingsGitmojiEnabled: (enabled: boolean) => void;
|
||||
setSettingsZenModel: (model: string | undefined) => void;
|
||||
setSettingsGitProviderId: (providerId: string | undefined) => void;
|
||||
setSettingsGitModelId: (modelId: string | undefined) => void;
|
||||
getResolvedGitGenerationModel: () => { providerId: string; modelId: string } | null;
|
||||
saveAgentModelSelection: (agentName: string, providerId: string, modelId: string) => void;
|
||||
getAgentModelSelection: (agentName: string) => { providerId: string; modelId: string } | null;
|
||||
checkConnection: () => Promise<boolean>;
|
||||
@@ -466,6 +557,8 @@ export const useConfigStore = create<ConfigStore>()(
|
||||
settingsAutoCreateWorktree: false,
|
||||
settingsGitmojiEnabled: false,
|
||||
settingsZenModel: undefined,
|
||||
settingsGitProviderId: undefined,
|
||||
settingsGitModelId: undefined,
|
||||
// Voice provider preference - load from localStorage or default to 'browser'
|
||||
voiceProvider: (() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
@@ -1007,6 +1100,36 @@ export const useConfigStore = create<ConfigStore>()(
|
||||
? get().providers
|
||||
: (get().directoryScoped[directoryKey]?.providers ?? []);
|
||||
|
||||
const existingGitProviderId = normalizeOptionalString(get().settingsGitProviderId);
|
||||
const existingGitModelId = normalizeOptionalString(get().settingsGitModelId);
|
||||
const existingZenModel = normalizeOptionalString(get().settingsZenModel);
|
||||
|
||||
const defaultGitProviderId = normalizeOptionalString(openChamberDefaults.gitProviderId);
|
||||
const defaultGitModelId = normalizeOptionalString(openChamberDefaults.gitModelId);
|
||||
const defaultZenModel = normalizeOptionalString(openChamberDefaults.zenModel);
|
||||
|
||||
const resolvedExistingGitSelection = resolveGitGenerationModelSelection({
|
||||
providers,
|
||||
settingsGitProviderId: existingGitProviderId,
|
||||
settingsGitModelId: existingGitModelId,
|
||||
settingsZenModel: existingZenModel,
|
||||
});
|
||||
|
||||
const resolvedDefaultGitSelection = resolveGitGenerationModelSelection({
|
||||
providers,
|
||||
settingsGitProviderId: defaultGitProviderId,
|
||||
settingsGitModelId: defaultGitModelId,
|
||||
settingsZenModel: defaultZenModel,
|
||||
});
|
||||
|
||||
const resolvedGitSelection = resolvedExistingGitSelection || resolvedDefaultGitSelection;
|
||||
const resolvedGitProviderId = resolvedGitSelection?.providerId;
|
||||
const resolvedGitModelId = resolvedGitSelection?.modelId;
|
||||
const resolvedZenModel =
|
||||
resolvedGitProviderId === GIT_UTILITY_PROVIDER_ID && resolvedGitModelId
|
||||
? resolvedGitModelId
|
||||
: (defaultZenModel || existingZenModel);
|
||||
|
||||
set((state) => {
|
||||
const baseSnapshot: DirectoryScopedConfig = state.directoryScoped[directoryKey] ?? {
|
||||
providers,
|
||||
@@ -1031,7 +1154,9 @@ export const useConfigStore = create<ConfigStore>()(
|
||||
settingsDefaultAgent: openChamberDefaults.defaultAgent,
|
||||
settingsAutoCreateWorktree: openChamberDefaults.autoCreateWorktree ?? false,
|
||||
settingsGitmojiEnabled: openChamberDefaults.gitmojiEnabled ?? false,
|
||||
settingsZenModel: openChamberDefaults.zenModel,
|
||||
settingsZenModel: resolvedZenModel,
|
||||
settingsGitProviderId: resolvedGitProviderId,
|
||||
settingsGitModelId: resolvedGitModelId,
|
||||
directoryScoped: {
|
||||
...state.directoryScoped,
|
||||
[directoryKey]: nextSnapshot,
|
||||
@@ -1045,6 +1170,37 @@ export const useConfigStore = create<ConfigStore>()(
|
||||
return nextState;
|
||||
});
|
||||
|
||||
const shouldPersistResolvedGitSelection =
|
||||
!!resolvedGitProviderId &&
|
||||
!!resolvedGitModelId &&
|
||||
(
|
||||
defaultGitProviderId !== resolvedGitProviderId ||
|
||||
defaultGitModelId !== resolvedGitModelId ||
|
||||
(
|
||||
resolvedGitProviderId === GIT_UTILITY_PROVIDER_ID &&
|
||||
resolvedZenModel !== defaultZenModel
|
||||
)
|
||||
);
|
||||
|
||||
if (shouldPersistResolvedGitSelection && resolvedGitProviderId && resolvedGitModelId) {
|
||||
const gitSettingsUpdate: {
|
||||
gitProviderId: string;
|
||||
gitModelId: string;
|
||||
zenModel?: string;
|
||||
} = {
|
||||
gitProviderId: resolvedGitProviderId,
|
||||
gitModelId: resolvedGitModelId,
|
||||
};
|
||||
|
||||
if (resolvedGitProviderId === GIT_UTILITY_PROVIDER_ID && resolvedZenModel) {
|
||||
gitSettingsUpdate.zenModel = resolvedZenModel;
|
||||
}
|
||||
|
||||
updateDesktopSettings(gitSettingsUpdate).catch(() => {
|
||||
// Ignore errors - best effort cleanup
|
||||
});
|
||||
}
|
||||
|
||||
if (safeAgents.length === 0) {
|
||||
set((state) => {
|
||||
const baseSnapshot: DirectoryScopedConfig = state.directoryScoped[directoryKey] ?? {
|
||||
@@ -1460,6 +1616,24 @@ export const useConfigStore = create<ConfigStore>()(
|
||||
set({ settingsZenModel: model });
|
||||
},
|
||||
|
||||
setSettingsGitProviderId: (providerId: string | undefined) => {
|
||||
set({ settingsGitProviderId: providerId });
|
||||
},
|
||||
|
||||
setSettingsGitModelId: (modelId: string | undefined) => {
|
||||
set({ settingsGitModelId: modelId });
|
||||
},
|
||||
|
||||
getResolvedGitGenerationModel: () => {
|
||||
const state = get();
|
||||
return resolveGitGenerationModelSelection({
|
||||
providers: state.providers,
|
||||
settingsGitProviderId: state.settingsGitProviderId,
|
||||
settingsGitModelId: state.settingsGitModelId,
|
||||
settingsZenModel: state.settingsZenModel,
|
||||
});
|
||||
},
|
||||
|
||||
setVoiceProvider: (provider: 'browser' | 'openai' | 'say') => {
|
||||
set({ voiceProvider: provider });
|
||||
if (typeof window !== 'undefined') {
|
||||
@@ -1670,6 +1844,8 @@ export const useConfigStore = create<ConfigStore>()(
|
||||
settingsAutoCreateWorktree: state.settingsAutoCreateWorktree,
|
||||
settingsGitmojiEnabled: state.settingsGitmojiEnabled,
|
||||
settingsZenModel: state.settingsZenModel,
|
||||
settingsGitProviderId: state.settingsGitProviderId,
|
||||
settingsGitModelId: state.settingsGitModelId,
|
||||
speechRate: state.speechRate,
|
||||
speechPitch: state.speechPitch,
|
||||
speechVolume: state.speechVolume,
|
||||
|
||||
Reference in New Issue
Block a user