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
@@ -12,10 +12,8 @@ import { useUIStore } from '@/stores/useUIStore';
|
|||||||
import { getRegisteredRuntimeAPIs } from '@/contexts/runtimeAPIRegistry';
|
import { getRegisteredRuntimeAPIs } from '@/contexts/runtimeAPIRegistry';
|
||||||
import { getModifierLabel, cn } from '@/lib/utils';
|
import { getModifierLabel, cn } from '@/lib/utils';
|
||||||
|
|
||||||
interface ZenModel {
|
const UTILITY_PROVIDER_ID = 'zen';
|
||||||
id: string;
|
const UTILITY_PREFERRED_MODEL_ID = 'big-pickle';
|
||||||
owned_by?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const getDisplayModel = (
|
const getDisplayModel = (
|
||||||
storedModel: string | undefined
|
storedModel: string | undefined
|
||||||
@@ -26,9 +24,46 @@ const getDisplayModel = (
|
|||||||
return { providerId: parts[0], modelId: parts[1] };
|
return { providerId: parts[0], modelId: parts[1] };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return empty values when no model is explicitly set
|
return { providerId: '', modelId: '' };
|
||||||
// This allows showing "Not selected" instead of a fallback
|
};
|
||||||
|
|
||||||
|
const getUtilityDisplayModel = (
|
||||||
|
storedGitProviderId: string | undefined,
|
||||||
|
storedGitModelId: string | undefined,
|
||||||
|
zenModel: string | undefined,
|
||||||
|
providers: Array<{ id: string; models: Array<{ id: string }> }>
|
||||||
|
): { providerId: string; modelId: string } => {
|
||||||
|
if (storedGitProviderId && storedGitModelId) {
|
||||||
|
const provider = providers.find((p) => p.id === storedGitProviderId);
|
||||||
|
if (provider?.models.some((m) => m.id === storedGitModelId)) {
|
||||||
|
return { providerId: storedGitProviderId, modelId: storedGitModelId };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const utilityProvider = providers.find((p) => p.id === UTILITY_PROVIDER_ID);
|
||||||
|
if (zenModel && utilityProvider?.models.some((m) => m.id === zenModel)) {
|
||||||
|
return { providerId: UTILITY_PROVIDER_ID, modelId: zenModel };
|
||||||
|
}
|
||||||
|
|
||||||
|
const preferredUtilityModel = utilityProvider?.models.find((m) => m.id === UTILITY_PREFERRED_MODEL_ID);
|
||||||
|
if (preferredUtilityModel) {
|
||||||
|
return { providerId: UTILITY_PROVIDER_ID, modelId: preferredUtilityModel.id };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (utilityProvider?.models.length) {
|
||||||
|
const randomIndex = Math.floor(Math.random() * utilityProvider.models.length);
|
||||||
|
const randomModel = utilityProvider.models[randomIndex];
|
||||||
|
if (randomModel?.id) {
|
||||||
|
return { providerId: UTILITY_PROVIDER_ID, modelId: randomModel.id };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstProvider = providers[0];
|
||||||
|
if (firstProvider?.models[0]) {
|
||||||
|
return { providerId: firstProvider.id, modelId: firstProvider.models[0].id };
|
||||||
|
}
|
||||||
|
|
||||||
return { providerId: '', modelId: '' };
|
return { providerId: '', modelId: '' };
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -44,6 +79,10 @@ export const DefaultsSettings: React.FC = () => {
|
|||||||
const setSettingsAutoCreateWorktree = useConfigStore((state) => state.setSettingsAutoCreateWorktree);
|
const setSettingsAutoCreateWorktree = useConfigStore((state) => state.setSettingsAutoCreateWorktree);
|
||||||
const settingsZenModel = useConfigStore((state) => state.settingsZenModel);
|
const settingsZenModel = useConfigStore((state) => state.settingsZenModel);
|
||||||
const setSettingsZenModel = useConfigStore((state) => state.setSettingsZenModel);
|
const setSettingsZenModel = useConfigStore((state) => state.setSettingsZenModel);
|
||||||
|
const settingsGitProviderId = useConfigStore((state) => state.settingsGitProviderId);
|
||||||
|
const settingsGitModelId = useConfigStore((state) => state.settingsGitModelId);
|
||||||
|
const setSettingsGitProviderId = useConfigStore((state) => state.setSettingsGitProviderId);
|
||||||
|
const setSettingsGitModelId = useConfigStore((state) => state.setSettingsGitModelId);
|
||||||
const showDeletionDialog = useUIStore((state) => state.showDeletionDialog);
|
const showDeletionDialog = useUIStore((state) => state.showDeletionDialog);
|
||||||
const setShowDeletionDialog = useUIStore((state) => state.setShowDeletionDialog);
|
const setShowDeletionDialog = useUIStore((state) => state.setShowDeletionDialog);
|
||||||
const providers = useConfigStore((state) => state.providers);
|
const providers = useConfigStore((state) => state.providers);
|
||||||
@@ -52,54 +91,25 @@ export const DefaultsSettings: React.FC = () => {
|
|||||||
const [defaultVariant, setDefaultVariant] = React.useState<string | undefined>();
|
const [defaultVariant, setDefaultVariant] = React.useState<string | undefined>();
|
||||||
const [defaultAgent, setDefaultAgent] = React.useState<string | undefined>();
|
const [defaultAgent, setDefaultAgent] = React.useState<string | undefined>();
|
||||||
const [isLoading, setIsLoading] = React.useState(true);
|
const [isLoading, setIsLoading] = React.useState(true);
|
||||||
const [zenModels, setZenModels] = React.useState<ZenModel[]>([]);
|
|
||||||
const [zenModelsLoading, setZenModelsLoading] = React.useState(true);
|
|
||||||
|
|
||||||
const parsedModel = React.useMemo(() => {
|
|
||||||
return getDisplayModel(defaultModel);
|
|
||||||
}, [defaultModel]);
|
|
||||||
|
|
||||||
|
const parsedModel = React.useMemo(() => getDisplayModel(defaultModel), [defaultModel]);
|
||||||
|
const utilityDisplayModel = React.useMemo(() => {
|
||||||
|
return getUtilityDisplayModel(settingsGitProviderId, settingsGitModelId, settingsZenModel, providers);
|
||||||
|
}, [settingsGitProviderId, settingsGitModelId, settingsZenModel, providers]);
|
||||||
const isVSCode = React.useMemo(() => isVSCodeRuntime(), []);
|
const isVSCode = React.useMemo(() => isVSCodeRuntime(), []);
|
||||||
|
|
||||||
// Load zen models list
|
|
||||||
React.useEffect(() => {
|
|
||||||
const loadZenModels = async () => {
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/zen/models', {
|
|
||||||
method: 'GET',
|
|
||||||
headers: { Accept: 'application/json' },
|
|
||||||
});
|
|
||||||
if (response.ok) {
|
|
||||||
const data = await response.json() as { models?: ZenModel[] };
|
|
||||||
if (Array.isArray(data?.models)) {
|
|
||||||
setZenModels(data.models);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('Failed to load zen models:', error);
|
|
||||||
} finally {
|
|
||||||
setZenModelsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
loadZenModels();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Resolve which zen model to display as selected
|
|
||||||
const selectedZenModel = React.useMemo(() => {
|
|
||||||
if (settingsZenModel && zenModels.some((m) => m.id === settingsZenModel)) {
|
|
||||||
return settingsZenModel;
|
|
||||||
}
|
|
||||||
// Default to first free model in the list
|
|
||||||
return zenModels[0]?.id ?? '';
|
|
||||||
}, [settingsZenModel, zenModels]);
|
|
||||||
|
|
||||||
// Load current settings
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const loadSettings = async () => {
|
const loadSettings = async () => {
|
||||||
try {
|
try {
|
||||||
let data: { defaultModel?: string; defaultVariant?: string; defaultAgent?: string; zenModel?: string } | null = null;
|
let data: {
|
||||||
|
defaultModel?: string;
|
||||||
|
defaultVariant?: string;
|
||||||
|
defaultAgent?: string;
|
||||||
|
zenModel?: string;
|
||||||
|
gitProviderId?: string;
|
||||||
|
gitModelId?: string;
|
||||||
|
} | null = null;
|
||||||
|
|
||||||
// 1. Runtime settings API (VSCode)
|
|
||||||
if (!data) {
|
if (!data) {
|
||||||
const runtimeSettings = getRegisteredRuntimeAPIs()?.settings;
|
const runtimeSettings = getRegisteredRuntimeAPIs()?.settings;
|
||||||
if (runtimeSettings) {
|
if (runtimeSettings) {
|
||||||
@@ -109,9 +119,23 @@ export const DefaultsSettings: React.FC = () => {
|
|||||||
if (settings) {
|
if (settings) {
|
||||||
data = {
|
data = {
|
||||||
defaultModel: typeof settings.defaultModel === 'string' ? settings.defaultModel : undefined,
|
defaultModel: typeof settings.defaultModel === 'string' ? settings.defaultModel : undefined,
|
||||||
defaultVariant: typeof (settings as Record<string, unknown>).defaultVariant === 'string' ? ((settings as Record<string, unknown>).defaultVariant as string) : undefined,
|
defaultVariant:
|
||||||
|
typeof (settings as Record<string, unknown>).defaultVariant === 'string'
|
||||||
|
? ((settings as Record<string, unknown>).defaultVariant as string)
|
||||||
|
: undefined,
|
||||||
defaultAgent: typeof settings.defaultAgent === 'string' ? settings.defaultAgent : undefined,
|
defaultAgent: typeof settings.defaultAgent === 'string' ? settings.defaultAgent : undefined,
|
||||||
zenModel: typeof (settings as Record<string, unknown>).zenModel === 'string' ? ((settings as Record<string, unknown>).zenModel as string) : undefined,
|
zenModel:
|
||||||
|
typeof (settings as Record<string, unknown>).zenModel === 'string'
|
||||||
|
? ((settings as Record<string, unknown>).zenModel as string)
|
||||||
|
: undefined,
|
||||||
|
gitProviderId:
|
||||||
|
typeof (settings as Record<string, unknown>).gitProviderId === 'string'
|
||||||
|
? ((settings as Record<string, unknown>).gitProviderId as string)
|
||||||
|
: undefined,
|
||||||
|
gitModelId:
|
||||||
|
typeof (settings as Record<string, unknown>).gitModelId === 'string'
|
||||||
|
? ((settings as Record<string, unknown>).gitModelId as string)
|
||||||
|
: undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
@@ -120,7 +144,6 @@ export const DefaultsSettings: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Fetch API (Web/server)
|
|
||||||
if (!data) {
|
if (!data) {
|
||||||
const response = await fetch('/api/config/settings', {
|
const response = await fetch('/api/config/settings', {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
@@ -131,25 +154,39 @@ export const DefaultsSettings: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
const model = typeof data.defaultModel === 'string' && data.defaultModel.trim().length > 0 ? data.defaultModel.trim() : undefined;
|
const model =
|
||||||
const variant = typeof data.defaultVariant === 'string' && data.defaultVariant.trim().length > 0 ? data.defaultVariant.trim() : undefined;
|
typeof data.defaultModel === 'string' && data.defaultModel.trim().length > 0
|
||||||
const agent = typeof data.defaultAgent === 'string' && data.defaultAgent.trim().length > 0 ? data.defaultAgent.trim() : undefined;
|
? data.defaultModel.trim()
|
||||||
const zen = typeof data.zenModel === 'string' && data.zenModel.trim().length > 0 ? data.zenModel.trim() : undefined;
|
: undefined;
|
||||||
|
const variant =
|
||||||
|
typeof data.defaultVariant === 'string' && data.defaultVariant.trim().length > 0
|
||||||
|
? data.defaultVariant.trim()
|
||||||
|
: undefined;
|
||||||
|
const agent =
|
||||||
|
typeof data.defaultAgent === 'string' && data.defaultAgent.trim().length > 0
|
||||||
|
? data.defaultAgent.trim()
|
||||||
|
: undefined;
|
||||||
|
const zen =
|
||||||
|
typeof data.zenModel === 'string' && data.zenModel.trim().length > 0
|
||||||
|
? data.zenModel.trim()
|
||||||
|
: undefined;
|
||||||
|
const gitProviderId =
|
||||||
|
typeof data.gitProviderId === 'string' && data.gitProviderId.trim().length > 0
|
||||||
|
? data.gitProviderId.trim()
|
||||||
|
: undefined;
|
||||||
|
const gitModelId =
|
||||||
|
typeof data.gitModelId === 'string' && data.gitModelId.trim().length > 0
|
||||||
|
? data.gitModelId.trim()
|
||||||
|
: undefined;
|
||||||
|
|
||||||
if (model !== undefined) {
|
if (model !== undefined) setDefaultModel(model);
|
||||||
setDefaultModel(model);
|
if (variant !== undefined) setDefaultVariant(variant);
|
||||||
}
|
if (agent !== undefined) setDefaultAgent(agent);
|
||||||
if (variant !== undefined) {
|
if (zen !== undefined) setSettingsZenModel(zen);
|
||||||
setDefaultVariant(variant);
|
if (gitProviderId !== undefined) setSettingsGitProviderId(gitProviderId);
|
||||||
}
|
if (gitModelId !== undefined) setSettingsGitModelId(gitModelId);
|
||||||
if (agent !== undefined) {
|
}
|
||||||
setDefaultAgent(agent);
|
|
||||||
}
|
|
||||||
if (zen !== undefined) {
|
|
||||||
setSettingsZenModel(zen);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Failed to load defaults settings:', error);
|
console.warn('Failed to load defaults settings:', error);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -157,88 +194,78 @@ export const DefaultsSettings: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
loadSettings();
|
loadSettings();
|
||||||
}, [setSettingsZenModel]);
|
}, [setSettingsGitModelId, setSettingsGitProviderId, setSettingsZenModel]);
|
||||||
|
|
||||||
|
const handleModelChange = React.useCallback(
|
||||||
|
async (providerId: string, modelId: string) => {
|
||||||
|
const newValue = providerId && modelId ? `${providerId}/${modelId}` : undefined;
|
||||||
|
setDefaultModel(newValue);
|
||||||
|
setDefaultVariant(undefined);
|
||||||
|
setSettingsDefaultVariant(undefined);
|
||||||
|
setCurrentVariant(undefined);
|
||||||
|
setSettingsDefaultModel(newValue);
|
||||||
|
|
||||||
const handleModelChange = React.useCallback(async (providerId: string, modelId: string) => {
|
if (providerId && modelId) {
|
||||||
const newValue = providerId && modelId ? `${providerId}/${modelId}` : undefined;
|
const provider = providers.find((p) => p.id === providerId);
|
||||||
setDefaultModel(newValue);
|
if (provider) {
|
||||||
|
setProvider(providerId);
|
||||||
// Reset variant when model changes (model-specific)
|
setModel(modelId);
|
||||||
setDefaultVariant(undefined);
|
}
|
||||||
setSettingsDefaultVariant(undefined);
|
|
||||||
setCurrentVariant(undefined);
|
|
||||||
|
|
||||||
// Update config store settings default (used by setAgent logic)
|
|
||||||
setSettingsDefaultModel(newValue);
|
|
||||||
|
|
||||||
// Also update current model immediately so new sessions use this model
|
|
||||||
if (providerId && modelId) {
|
|
||||||
const provider = providers.find((p) => p.id === providerId);
|
|
||||||
if (provider) {
|
|
||||||
setProvider(providerId);
|
|
||||||
setModel(modelId);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await updateDesktopSettings({
|
await updateDesktopSettings({ defaultModel: newValue ?? '', defaultVariant: '' });
|
||||||
defaultModel: newValue ?? '',
|
const response = await fetch('/api/config/settings', {
|
||||||
defaultVariant: '',
|
method: 'PUT',
|
||||||
});
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ defaultModel: newValue }),
|
||||||
{
|
});
|
||||||
const response = await fetch('/api/config/settings', {
|
if (!response.ok) {
|
||||||
method: 'PUT',
|
console.warn('Failed to save default model to server:', response.status, response.statusText);
|
||||||
headers: { 'Content-Type': 'application/json' },
|
}
|
||||||
body: JSON.stringify({ defaultModel: newValue }),
|
} catch (error) {
|
||||||
});
|
console.warn('Failed to save default model:', error);
|
||||||
if (!response.ok) {
|
}
|
||||||
console.warn('Failed to save default model to server:', response.status, response.statusText);
|
},
|
||||||
}
|
[providers, setCurrentVariant, setModel, setProvider, setSettingsDefaultModel, setSettingsDefaultVariant]
|
||||||
}
|
);
|
||||||
} catch (error) {
|
|
||||||
console.warn('Failed to save default model:', error);
|
|
||||||
}
|
|
||||||
}, [providers, setCurrentVariant, setProvider, setModel, setSettingsDefaultModel, setSettingsDefaultVariant]);
|
|
||||||
|
|
||||||
const DEFAULT_VARIANT_VALUE = '__default__';
|
const DEFAULT_VARIANT_VALUE = '__default__';
|
||||||
|
|
||||||
const handleVariantChange = React.useCallback(async (variant: string) => {
|
const handleVariantChange = React.useCallback(
|
||||||
const newValue = variant === DEFAULT_VARIANT_VALUE ? undefined : (variant || undefined);
|
async (variant: string) => {
|
||||||
setDefaultVariant(newValue);
|
const newValue = variant === DEFAULT_VARIANT_VALUE ? undefined : variant || undefined;
|
||||||
setSettingsDefaultVariant(newValue);
|
setDefaultVariant(newValue);
|
||||||
setCurrentVariant(newValue);
|
setSettingsDefaultVariant(newValue);
|
||||||
|
setCurrentVariant(newValue);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await updateDesktopSettings({
|
await updateDesktopSettings({ defaultVariant: newValue ?? '' });
|
||||||
defaultVariant: newValue ?? '',
|
} catch (error) {
|
||||||
});
|
console.warn('Failed to save default variant:', error);
|
||||||
} catch (error) {
|
}
|
||||||
console.warn('Failed to save default variant:', error);
|
},
|
||||||
}
|
[setCurrentVariant, setSettingsDefaultVariant]
|
||||||
}, [setCurrentVariant, setSettingsDefaultVariant]);
|
);
|
||||||
|
|
||||||
const handleAgentChange = React.useCallback(async (agentName: string) => {
|
const handleAgentChange = React.useCallback(
|
||||||
const newValue = agentName || undefined;
|
async (agentName: string) => {
|
||||||
setDefaultAgent(newValue);
|
const newValue = agentName || undefined;
|
||||||
|
setDefaultAgent(newValue);
|
||||||
|
setSettingsDefaultAgent(newValue);
|
||||||
|
|
||||||
// Update config store settings default
|
if (agentName) {
|
||||||
setSettingsDefaultAgent(newValue);
|
setAgent(agentName);
|
||||||
|
}
|
||||||
|
|
||||||
// Update current agent (setAgent will respect settingsDefaultModel)
|
try {
|
||||||
if (agentName) {
|
await updateDesktopSettings({ defaultAgent: newValue ?? '' });
|
||||||
setAgent(agentName);
|
} catch (error) {
|
||||||
}
|
console.warn('Failed to save default agent:', error);
|
||||||
|
}
|
||||||
try {
|
},
|
||||||
await updateDesktopSettings({
|
[setAgent, setSettingsDefaultAgent]
|
||||||
defaultAgent: newValue ?? '',
|
);
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('Failed to save default agent:', error);
|
|
||||||
}
|
|
||||||
}, [setAgent, setSettingsDefaultAgent]);
|
|
||||||
|
|
||||||
const availableVariants = React.useMemo(() => {
|
const availableVariants = React.useMemo(() => {
|
||||||
if (!parsedModel.providerId || !parsedModel.modelId) return [];
|
if (!parsedModel.providerId || !parsedModel.modelId) return [];
|
||||||
@@ -247,9 +274,7 @@ export const DefaultsSettings: React.FC = () => {
|
|||||||
| { variants?: Record<string, unknown> }
|
| { variants?: Record<string, unknown> }
|
||||||
| undefined;
|
| undefined;
|
||||||
const variants = model?.variants;
|
const variants = model?.variants;
|
||||||
if (!variants) {
|
if (!variants) return [];
|
||||||
return [];
|
|
||||||
}
|
|
||||||
return Object.keys(variants);
|
return Object.keys(variants);
|
||||||
}, [parsedModel.modelId, parsedModel.providerId, providers]);
|
}, [parsedModel.modelId, parsedModel.providerId, providers]);
|
||||||
|
|
||||||
@@ -266,27 +291,37 @@ export const DefaultsSettings: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}, [defaultVariant, setCurrentVariant, setSettingsDefaultVariant, supportsVariants]);
|
}, [defaultVariant, setCurrentVariant, setSettingsDefaultVariant, supportsVariants]);
|
||||||
|
|
||||||
const handleAutoWorktreeChange = React.useCallback(async (enabled: boolean) => {
|
const handleAutoWorktreeChange = React.useCallback(
|
||||||
setSettingsAutoCreateWorktree(enabled);
|
async (enabled: boolean) => {
|
||||||
try {
|
setSettingsAutoCreateWorktree(enabled);
|
||||||
await updateDesktopSettings({
|
try {
|
||||||
autoCreateWorktree: enabled,
|
await updateDesktopSettings({ autoCreateWorktree: enabled });
|
||||||
});
|
} catch (error) {
|
||||||
} catch (error) {
|
console.warn('Failed to save auto create worktree setting:', error);
|
||||||
console.warn('Failed to save auto create worktree setting:', error);
|
}
|
||||||
}
|
},
|
||||||
}, [setSettingsAutoCreateWorktree]);
|
[setSettingsAutoCreateWorktree]
|
||||||
|
);
|
||||||
|
|
||||||
const handleZenModelChange = React.useCallback(async (modelId: string) => {
|
const handleUtilityModelChange = React.useCallback(
|
||||||
setSettingsZenModel(modelId);
|
async (providerId: string, modelId: string) => {
|
||||||
try {
|
setSettingsGitProviderId(providerId);
|
||||||
await updateDesktopSettings({
|
setSettingsGitModelId(modelId);
|
||||||
zenModel: modelId,
|
if (providerId === UTILITY_PROVIDER_ID) {
|
||||||
});
|
setSettingsZenModel(modelId);
|
||||||
} catch (error) {
|
}
|
||||||
console.warn('Failed to save zen model setting:', error);
|
try {
|
||||||
}
|
await updateDesktopSettings({
|
||||||
}, [setSettingsZenModel]);
|
gitProviderId: providerId,
|
||||||
|
gitModelId: modelId,
|
||||||
|
...(providerId === UTILITY_PROVIDER_ID ? { zenModel: modelId } : {}),
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to save utility model setting:', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[setSettingsGitModelId, setSettingsGitProviderId, setSettingsZenModel]
|
||||||
|
);
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return null;
|
return null;
|
||||||
@@ -319,85 +354,65 @@ export const DefaultsSettings: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={cn("flex flex-col gap-2 py-1 sm:flex-row sm:items-center sm:gap-8")}>
|
<div className={cn('flex flex-col gap-2 py-1 sm:flex-row sm:items-center sm:gap-8')}>
|
||||||
<div className="flex min-w-0 flex-col sm:w-56 shrink-0">
|
<div className="flex min-w-0 flex-col sm:w-56 shrink-0">
|
||||||
<span className="typography-ui-label text-foreground">Default Model</span>
|
<span className="typography-ui-label text-foreground">Default Model</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex min-w-0 flex-1 items-center gap-2 sm:w-fit sm:flex-initial">
|
<div className="flex min-w-0 flex-1 items-center gap-2 sm:w-fit sm:flex-initial">
|
||||||
<ModelSelector
|
<ModelSelector providerId={parsedModel.providerId} modelId={parsedModel.modelId} onChange={handleModelChange} />
|
||||||
providerId={parsedModel.providerId}
|
|
||||||
modelId={parsedModel.modelId}
|
|
||||||
onChange={handleModelChange}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-2 py-1 sm:flex-row sm:items-center sm:gap-8">
|
<div className="flex flex-col gap-2 py-1 sm:flex-row sm:items-center sm:gap-8">
|
||||||
<div className="flex min-w-0 flex-col sm:w-56 shrink-0">
|
<div className="flex min-w-0 flex-col sm:w-56 shrink-0">
|
||||||
<span className="typography-ui-label text-foreground">Default Thinking</span>
|
<span className="typography-ui-label text-foreground">Default Thinking</span>
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2 sm:w-fit">
|
|
||||||
<Select value={defaultVariant ?? DEFAULT_VARIANT_VALUE} onValueChange={handleVariantChange} disabled={!supportsVariants}>
|
|
||||||
<SelectTrigger className="w-fit min-w-[120px]">
|
|
||||||
<SelectValue placeholder="Thinking" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value={DEFAULT_VARIANT_VALUE}>Default</SelectItem>
|
|
||||||
{availableVariants.map((variant) => (
|
|
||||||
<SelectItem key={variant} value={variant}>
|
|
||||||
{variant}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center gap-2 sm:w-fit">
|
||||||
|
<Select value={defaultVariant ?? DEFAULT_VARIANT_VALUE} onValueChange={handleVariantChange} disabled={!supportsVariants}>
|
||||||
|
<SelectTrigger className="w-fit min-w-[120px]">
|
||||||
|
<SelectValue placeholder="Thinking" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value={DEFAULT_VARIANT_VALUE}>Default</SelectItem>
|
||||||
|
{availableVariants.map((variant) => (
|
||||||
|
<SelectItem key={variant} value={variant}>
|
||||||
|
{variant}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-2 py-1 sm:flex-row sm:items-center sm:gap-8">
|
<div className="flex flex-col gap-2 py-1 sm:flex-row sm:items-center sm:gap-8">
|
||||||
<div className="flex min-w-0 flex-col sm:w-56 shrink-0">
|
<div className="flex min-w-0 flex-col sm:w-56 shrink-0">
|
||||||
<span className="typography-ui-label text-foreground">Default Agent</span>
|
<span className="typography-ui-label text-foreground">Default Agent</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex min-w-0 flex-1 items-center gap-2 sm:w-fit sm:flex-initial">
|
<div className="flex min-w-0 flex-1 items-center gap-2 sm:w-fit sm:flex-initial">
|
||||||
<AgentSelector
|
<AgentSelector agentName={defaultAgent || ''} onChange={handleAgentChange} />
|
||||||
agentName={defaultAgent || ''}
|
|
||||||
onChange={handleAgentChange}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-2 py-1 sm:flex-row sm:items-center sm:gap-8">
|
<div className="flex flex-col gap-2 py-1 sm:flex-row sm:items-center sm:gap-8">
|
||||||
<div className="flex min-w-0 flex-col sm:w-56 shrink-0">
|
<div className="flex min-w-0 flex-col sm:w-56 shrink-0">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="typography-ui-label text-foreground">Zen Model</span>
|
<span className="typography-ui-label text-foreground">Utility Model</span>
|
||||||
<Tooltip delayDuration={1000}>
|
<Tooltip delayDuration={1000}>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<RiInformationLine className="h-3.5 w-3.5 text-muted-foreground/60 cursor-help" />
|
<RiInformationLine className="h-3.5 w-3.5 text-muted-foreground/60 cursor-help" />
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent sideOffset={8} className="max-w-xs">
|
<TooltipContent sideOffset={8} className="max-w-xs">
|
||||||
The free model used for lightweight internal tasks like commit message generation, PR descriptions, notification summarization, and TTS text summarization.
|
The model used for lightweight background tasks like commit messages, PR descriptions, and summarization.
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 sm:w-fit">
|
<div className="flex min-w-0 flex-1 items-center gap-2 sm:w-fit sm:flex-initial">
|
||||||
{zenModelsLoading ? (
|
<ModelSelector
|
||||||
<span className="typography-meta text-muted-foreground">Loading models...</span>
|
providerId={utilityDisplayModel.providerId}
|
||||||
) : zenModels.length > 0 ? (
|
modelId={utilityDisplayModel.modelId}
|
||||||
<Select value={selectedZenModel} onValueChange={handleZenModelChange}>
|
onChange={handleUtilityModelChange}
|
||||||
<SelectTrigger className="w-fit min-w-[120px]">
|
/>
|
||||||
<SelectValue placeholder="Select model" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{zenModels.map((model) => (
|
|
||||||
<SelectItem key={model.id} value={model.id}>
|
|
||||||
{model.id}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
) : (
|
|
||||||
<span className="typography-meta text-muted-foreground">No free models available</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -414,11 +429,7 @@ export const DefaultsSettings: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Checkbox
|
<Checkbox checked={showDeletionDialog} onChange={setShowDeletionDialog} ariaLabel="Show deletion dialog" />
|
||||||
checked={showDeletionDialog}
|
|
||||||
onChange={setShowDeletionDialog}
|
|
||||||
ariaLabel="Show deletion dialog"
|
|
||||||
/>
|
|
||||||
<span className="typography-ui-label text-foreground">Show Deletion Dialog</span>
|
<span className="typography-ui-label text-foreground">Show Deletion Dialog</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -462,9 +473,7 @@ export const DefaultsSettings: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -818,11 +818,22 @@ export const GitView: React.FC<GitViewProps> = ({ mode = 'full' }) => {
|
|||||||
|
|
||||||
setIsGeneratingMessage(true);
|
setIsGeneratingMessage(true);
|
||||||
try {
|
try {
|
||||||
const zenModel = useConfigStore.getState().settingsZenModel;
|
const { getResolvedGitGenerationModel, settingsZenModel } = useConfigStore.getState();
|
||||||
|
const resolvedModel = getResolvedGitGenerationModel();
|
||||||
|
const options: { zenModel?: string; providerId?: string; modelId?: string } = {};
|
||||||
|
if (resolvedModel) {
|
||||||
|
options.providerId = resolvedModel.providerId;
|
||||||
|
options.modelId = resolvedModel.modelId;
|
||||||
|
if (resolvedModel.providerId === 'zen') {
|
||||||
|
options.zenModel = resolvedModel.modelId;
|
||||||
|
}
|
||||||
|
} else if (settingsZenModel) {
|
||||||
|
options.zenModel = settingsZenModel;
|
||||||
|
}
|
||||||
const { message } = await git.generateCommitMessage(
|
const { message } = await git.generateCommitMessage(
|
||||||
currentDirectory,
|
currentDirectory,
|
||||||
Array.from(selectedPaths),
|
Array.from(selectedPaths),
|
||||||
zenModel ? { zenModel } : undefined
|
Object.keys(options).length > 0 ? options : undefined
|
||||||
);
|
);
|
||||||
const subject = message.subject?.trim() ?? '';
|
const subject = message.subject?.trim() ?? '';
|
||||||
const highlights = Array.isArray(message.highlights) ? message.highlights : [];
|
const highlights = Array.isArray(message.highlights) ? message.highlights : [];
|
||||||
|
|||||||
@@ -1128,13 +1128,25 @@ export const PullRequestSection: React.FC<{
|
|||||||
if (!directory) return;
|
if (!directory) return;
|
||||||
setIsGenerating(true);
|
setIsGenerating(true);
|
||||||
try {
|
try {
|
||||||
const zenModel = useConfigStore.getState().settingsZenModel;
|
const { getResolvedGitGenerationModel, settingsZenModel } = useConfigStore.getState();
|
||||||
const generated = await generatePullRequestDescription(directory, {
|
const resolvedModel = getResolvedGitGenerationModel();
|
||||||
|
const payload: { base: string; head: string; context?: string; zenModel?: string; providerId?: string; modelId?: string } = {
|
||||||
base: targetBaseBranch,
|
base: targetBaseBranch,
|
||||||
head: branch,
|
head: branch,
|
||||||
context: additionalContext,
|
};
|
||||||
...(zenModel ? { zenModel } : {}),
|
if (additionalContext) {
|
||||||
});
|
payload.context = additionalContext;
|
||||||
|
}
|
||||||
|
if (resolvedModel) {
|
||||||
|
payload.providerId = resolvedModel.providerId;
|
||||||
|
payload.modelId = resolvedModel.modelId;
|
||||||
|
if (resolvedModel.providerId === 'zen') {
|
||||||
|
payload.zenModel = resolvedModel.modelId;
|
||||||
|
}
|
||||||
|
} else if (settingsZenModel) {
|
||||||
|
payload.zenModel = settingsZenModel;
|
||||||
|
}
|
||||||
|
const generated = await generatePullRequestDescription(directory, payload);
|
||||||
|
|
||||||
if (generated.title?.trim()) {
|
if (generated.title?.trim()) {
|
||||||
setTitle(generated.title.trim());
|
setTitle(generated.title.trim());
|
||||||
|
|||||||
@@ -390,10 +390,10 @@ export interface GitAPI {
|
|||||||
getGitBranches(directory: string): Promise<GitBranch>;
|
getGitBranches(directory: string): Promise<GitBranch>;
|
||||||
deleteGitBranch(directory: string, payload: GitDeleteBranchPayload): Promise<{ success: boolean }>;
|
deleteGitBranch(directory: string, payload: GitDeleteBranchPayload): Promise<{ success: boolean }>;
|
||||||
deleteRemoteBranch(directory: string, payload: GitDeleteRemoteBranchPayload): Promise<{ success: boolean }>;
|
deleteRemoteBranch(directory: string, payload: GitDeleteRemoteBranchPayload): Promise<{ success: boolean }>;
|
||||||
generateCommitMessage(directory: string, files: string[], options?: { zenModel?: string }): Promise<{ message: GeneratedCommitMessage }>;
|
generateCommitMessage(directory: string, files: string[], options?: { zenModel?: string; providerId?: string; modelId?: string }): Promise<{ message: GeneratedCommitMessage }>;
|
||||||
generatePullRequestDescription(
|
generatePullRequestDescription(
|
||||||
directory: string,
|
directory: string,
|
||||||
payload: { base: string; head: string; context?: string; zenModel?: string }
|
payload: { base: string; head: string; context?: string; zenModel?: string; providerId?: string; modelId?: string }
|
||||||
): Promise<GeneratedPullRequestDescription>;
|
): Promise<GeneratedPullRequestDescription>;
|
||||||
listGitWorktrees(directory: string): Promise<GitWorktreeInfo[]>;
|
listGitWorktrees(directory: string): Promise<GitWorktreeInfo[]>;
|
||||||
validateGitWorktree?(directory: string, payload: CreateGitWorktreePayload): Promise<GitWorktreeValidationResult>;
|
validateGitWorktree?(directory: string, payload: CreateGitWorktreePayload): Promise<GitWorktreeValidationResult>;
|
||||||
@@ -529,6 +529,8 @@ export interface SettingsPayload {
|
|||||||
directoryShowHidden?: boolean;
|
directoryShowHidden?: boolean;
|
||||||
filesViewShowGitignored?: boolean;
|
filesViewShowGitignored?: boolean;
|
||||||
openInAppId?: string;
|
openInAppId?: string;
|
||||||
|
gitProviderId?: string;
|
||||||
|
gitModelId?: string;
|
||||||
|
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,6 +93,8 @@ export type DesktopSettings = {
|
|||||||
queueModeEnabled?: boolean;
|
queueModeEnabled?: boolean;
|
||||||
gitmojiEnabled?: boolean;
|
gitmojiEnabled?: boolean;
|
||||||
zenModel?: string;
|
zenModel?: string;
|
||||||
|
gitProviderId?: string;
|
||||||
|
gitModelId?: string;
|
||||||
toolCallExpansion?: 'collapsed' | 'activity' | 'detailed';
|
toolCallExpansion?: 'collapsed' | 'activity' | 'detailed';
|
||||||
fontSize?: number;
|
fontSize?: number;
|
||||||
terminalFontSize?: number;
|
terminalFontSize?: number;
|
||||||
|
|||||||
@@ -105,16 +105,16 @@ export async function deleteRemoteBranch(directory: string, payload: import('./a
|
|||||||
export async function generateCommitMessage(
|
export async function generateCommitMessage(
|
||||||
directory: string,
|
directory: string,
|
||||||
files: string[],
|
files: string[],
|
||||||
options?: { zenModel?: string }
|
options?: { zenModel?: string; providerId?: string; modelId?: string }
|
||||||
): Promise<{ message: import('./api/types').GeneratedCommitMessage }> {
|
): Promise<{ message: import('./api/types').GeneratedCommitMessage }> {
|
||||||
const runtime = getRuntimeGit();
|
const runtime = getRuntimeGit();
|
||||||
if (runtime) return runtime.generateCommitMessage(directory, files);
|
if (runtime) return runtime.generateCommitMessage(directory, files, options);
|
||||||
return gitHttp.generateCommitMessage(directory, files, options);
|
return gitHttp.generateCommitMessage(directory, files, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function generatePullRequestDescription(
|
export async function generatePullRequestDescription(
|
||||||
directory: string,
|
directory: string,
|
||||||
payload: { base: string; head: string; context?: string; zenModel?: string }
|
payload: { base: string; head: string; context?: string; zenModel?: string; providerId?: string; modelId?: string }
|
||||||
): Promise<import('./api/types').GeneratedPullRequestDescription> {
|
): Promise<import('./api/types').GeneratedPullRequestDescription> {
|
||||||
const runtime = getRuntimeGit();
|
const runtime = getRuntimeGit();
|
||||||
if (runtime?.generatePullRequestDescription) {
|
if (runtime?.generatePullRequestDescription) {
|
||||||
|
|||||||
@@ -210,7 +210,7 @@ export async function deleteRemoteBranch(directory: string, payload: GitDeleteRe
|
|||||||
export async function generateCommitMessage(
|
export async function generateCommitMessage(
|
||||||
directory: string,
|
directory: string,
|
||||||
files: string[],
|
files: string[],
|
||||||
options?: { zenModel?: string }
|
options?: { zenModel?: string; providerId?: string; modelId?: string }
|
||||||
): Promise<{ message: GeneratedCommitMessage }> {
|
): Promise<{ message: GeneratedCommitMessage }> {
|
||||||
if (!Array.isArray(files) || files.length === 0) {
|
if (!Array.isArray(files) || files.length === 0) {
|
||||||
throw new Error('No files provided to generate commit message');
|
throw new Error('No files provided to generate commit message');
|
||||||
@@ -220,6 +220,12 @@ export async function generateCommitMessage(
|
|||||||
if (options?.zenModel) {
|
if (options?.zenModel) {
|
||||||
body.zenModel = options.zenModel;
|
body.zenModel = options.zenModel;
|
||||||
}
|
}
|
||||||
|
if (options?.providerId) {
|
||||||
|
body.providerId = options.providerId;
|
||||||
|
}
|
||||||
|
if (options?.modelId) {
|
||||||
|
body.modelId = options.modelId;
|
||||||
|
}
|
||||||
|
|
||||||
const response = await fetch(buildUrl(`${API_BASE}/commit-message`, directory), {
|
const response = await fetch(buildUrl(`${API_BASE}/commit-message`, directory), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -259,20 +265,26 @@ export async function generateCommitMessage(
|
|||||||
|
|
||||||
export async function generatePullRequestDescription(
|
export async function generatePullRequestDescription(
|
||||||
directory: string,
|
directory: string,
|
||||||
payload: { base: string; head: string; context?: string; zenModel?: string }
|
payload: { base: string; head: string; context?: string; zenModel?: string; providerId?: string; modelId?: string }
|
||||||
): Promise<{ title: string; body: string }> {
|
): Promise<{ title: string; body: string }> {
|
||||||
const { base, head, context, zenModel } = payload;
|
const { base, head, context, zenModel, providerId, modelId } = payload;
|
||||||
if (!base || !head) {
|
if (!base || !head) {
|
||||||
throw new Error('base and head are required');
|
throw new Error('base and head are required');
|
||||||
}
|
}
|
||||||
|
|
||||||
const requestBody: { base: string; head: string; context?: string; zenModel?: string } = { base, head };
|
const requestBody: { base: string; head: string; context?: string; zenModel?: string; providerId?: string; modelId?: string } = { base, head };
|
||||||
if (context?.trim()) {
|
if (context?.trim()) {
|
||||||
requestBody.context = context.trim();
|
requestBody.context = context.trim();
|
||||||
}
|
}
|
||||||
if (zenModel) {
|
if (zenModel) {
|
||||||
requestBody.zenModel = zenModel;
|
requestBody.zenModel = zenModel;
|
||||||
}
|
}
|
||||||
|
if (providerId) {
|
||||||
|
requestBody.providerId = providerId;
|
||||||
|
}
|
||||||
|
if (modelId) {
|
||||||
|
requestBody.modelId = modelId;
|
||||||
|
}
|
||||||
|
|
||||||
const response = await fetch(buildUrl(`${API_BASE}/pr-description`, directory), {
|
const response = await fetch(buildUrl(`${API_BASE}/pr-description`, directory), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ const MODELS_DEV_PROXY_URL = "/api/openchamber/models-metadata";
|
|||||||
|
|
||||||
const FALLBACK_PROVIDER_ID = "opencode";
|
const FALLBACK_PROVIDER_ID = "opencode";
|
||||||
const FALLBACK_MODEL_ID = "big-pickle";
|
const FALLBACK_MODEL_ID = "big-pickle";
|
||||||
|
const GIT_UTILITY_PROVIDER_ID = "zen";
|
||||||
|
const GIT_UTILITY_PREFERRED_MODEL_ID = "big-pickle";
|
||||||
|
|
||||||
interface OpenChamberDefaults {
|
interface OpenChamberDefaults {
|
||||||
defaultModel?: string;
|
defaultModel?: string;
|
||||||
@@ -26,6 +28,8 @@ interface OpenChamberDefaults {
|
|||||||
autoCreateWorktree?: boolean;
|
autoCreateWorktree?: boolean;
|
||||||
gitmojiEnabled?: boolean;
|
gitmojiEnabled?: boolean;
|
||||||
zenModel?: string;
|
zenModel?: string;
|
||||||
|
gitProviderId?: string;
|
||||||
|
gitModelId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchOpenChamberDefaults = async (): Promise<OpenChamberDefaults> => {
|
const fetchOpenChamberDefaults = async (): Promise<OpenChamberDefaults> => {
|
||||||
@@ -42,6 +46,8 @@ const fetchOpenChamberDefaults = async (): Promise<OpenChamberDefaults> => {
|
|||||||
const defaultAgent = typeof data?.defaultAgent === 'string' ? data.defaultAgent.trim() : '';
|
const defaultAgent = typeof data?.defaultAgent === 'string' ? data.defaultAgent.trim() : '';
|
||||||
const gitmojiEnabled = typeof data?.gitmojiEnabled === 'boolean' ? data.gitmojiEnabled : undefined;
|
const gitmojiEnabled = typeof data?.gitmojiEnabled === 'boolean' ? data.gitmojiEnabled : undefined;
|
||||||
const zenModel = typeof data?.zenModel === 'string' ? data.zenModel.trim() : '';
|
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 {
|
return {
|
||||||
defaultModel: defaultModel.length > 0 ? defaultModel : undefined,
|
defaultModel: defaultModel.length > 0 ? defaultModel : undefined,
|
||||||
@@ -50,6 +56,8 @@ const fetchOpenChamberDefaults = async (): Promise<OpenChamberDefaults> => {
|
|||||||
autoCreateWorktree: typeof data?.autoCreateWorktree === 'boolean' ? data.autoCreateWorktree : undefined,
|
autoCreateWorktree: typeof data?.autoCreateWorktree === 'boolean' ? data.autoCreateWorktree : undefined,
|
||||||
gitmojiEnabled,
|
gitmojiEnabled,
|
||||||
zenModel: zenModel.length > 0 ? zenModel : undefined,
|
zenModel: zenModel.length > 0 ? zenModel : undefined,
|
||||||
|
gitProviderId: gitProviderId.length > 0 ? gitProviderId : undefined,
|
||||||
|
gitModelId: gitModelId.length > 0 ? gitModelId : undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
@@ -71,6 +79,8 @@ const fetchOpenChamberDefaults = async (): Promise<OpenChamberDefaults> => {
|
|||||||
const defaultAgent = typeof data?.defaultAgent === 'string' ? data.defaultAgent.trim() : '';
|
const defaultAgent = typeof data?.defaultAgent === 'string' ? data.defaultAgent.trim() : '';
|
||||||
const gitmojiEnabled = typeof data?.gitmojiEnabled === 'boolean' ? data.gitmojiEnabled : undefined;
|
const gitmojiEnabled = typeof data?.gitmojiEnabled === 'boolean' ? data.gitmojiEnabled : undefined;
|
||||||
const zenModel = typeof data?.zenModel === 'string' ? data.zenModel.trim() : '';
|
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 {
|
return {
|
||||||
defaultModel: defaultModel.length > 0 ? defaultModel : undefined,
|
defaultModel: defaultModel.length > 0 ? defaultModel : undefined,
|
||||||
@@ -79,6 +89,8 @@ const fetchOpenChamberDefaults = async (): Promise<OpenChamberDefaults> => {
|
|||||||
autoCreateWorktree: typeof data?.autoCreateWorktree === 'boolean' ? data.autoCreateWorktree : undefined,
|
autoCreateWorktree: typeof data?.autoCreateWorktree === 'boolean' ? data.autoCreateWorktree : undefined,
|
||||||
gitmojiEnabled,
|
gitmojiEnabled,
|
||||||
zenModel: zenModel.length > 0 ? zenModel : undefined,
|
zenModel: zenModel.length > 0 ? zenModel : undefined,
|
||||||
|
gitProviderId: gitProviderId.length > 0 ? gitProviderId : undefined,
|
||||||
|
gitModelId: gitModelId.length > 0 ? gitModelId : undefined,
|
||||||
};
|
};
|
||||||
} catch {
|
} catch {
|
||||||
return {};
|
return {};
|
||||||
@@ -103,6 +115,80 @@ const isPrimaryMode = (mode?: string) => mode === "primary" || mode === "all" ||
|
|||||||
type ProviderModel = Provider["models"][string];
|
type ProviderModel = Provider["models"][string];
|
||||||
type ProviderWithModelList = Omit<Provider, "models"> & { models: ProviderModel[] };
|
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 {
|
interface ModelsDevModelEntry {
|
||||||
id?: string;
|
id?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
@@ -372,6 +458,8 @@ interface ConfigStore {
|
|||||||
settingsAutoCreateWorktree: boolean;
|
settingsAutoCreateWorktree: boolean;
|
||||||
settingsGitmojiEnabled: boolean;
|
settingsGitmojiEnabled: boolean;
|
||||||
settingsZenModel: string | undefined;
|
settingsZenModel: string | undefined;
|
||||||
|
settingsGitProviderId: string | undefined;
|
||||||
|
settingsGitModelId: string | undefined;
|
||||||
// Voice provider preference ('browser', 'openai', or 'say' for macOS)
|
// Voice provider preference ('browser', 'openai', or 'say' for macOS)
|
||||||
voiceProvider: 'browser' | 'openai' | 'say';
|
voiceProvider: 'browser' | 'openai' | 'say';
|
||||||
setVoiceProvider: (provider: 'browser' | 'openai' | 'say') => void;
|
setVoiceProvider: (provider: 'browser' | 'openai' | 'say') => void;
|
||||||
@@ -421,6 +509,9 @@ interface ConfigStore {
|
|||||||
setSettingsAutoCreateWorktree: (enabled: boolean) => void;
|
setSettingsAutoCreateWorktree: (enabled: boolean) => void;
|
||||||
setSettingsGitmojiEnabled: (enabled: boolean) => void;
|
setSettingsGitmojiEnabled: (enabled: boolean) => void;
|
||||||
setSettingsZenModel: (model: string | undefined) => 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;
|
saveAgentModelSelection: (agentName: string, providerId: string, modelId: string) => void;
|
||||||
getAgentModelSelection: (agentName: string) => { providerId: string; modelId: string } | null;
|
getAgentModelSelection: (agentName: string) => { providerId: string; modelId: string } | null;
|
||||||
checkConnection: () => Promise<boolean>;
|
checkConnection: () => Promise<boolean>;
|
||||||
@@ -466,6 +557,8 @@ export const useConfigStore = create<ConfigStore>()(
|
|||||||
settingsAutoCreateWorktree: false,
|
settingsAutoCreateWorktree: false,
|
||||||
settingsGitmojiEnabled: false,
|
settingsGitmojiEnabled: false,
|
||||||
settingsZenModel: undefined,
|
settingsZenModel: undefined,
|
||||||
|
settingsGitProviderId: undefined,
|
||||||
|
settingsGitModelId: undefined,
|
||||||
// Voice provider preference - load from localStorage or default to 'browser'
|
// Voice provider preference - load from localStorage or default to 'browser'
|
||||||
voiceProvider: (() => {
|
voiceProvider: (() => {
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
@@ -1007,6 +1100,36 @@ export const useConfigStore = create<ConfigStore>()(
|
|||||||
? get().providers
|
? get().providers
|
||||||
: (get().directoryScoped[directoryKey]?.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) => {
|
set((state) => {
|
||||||
const baseSnapshot: DirectoryScopedConfig = state.directoryScoped[directoryKey] ?? {
|
const baseSnapshot: DirectoryScopedConfig = state.directoryScoped[directoryKey] ?? {
|
||||||
providers,
|
providers,
|
||||||
@@ -1031,7 +1154,9 @@ export const useConfigStore = create<ConfigStore>()(
|
|||||||
settingsDefaultAgent: openChamberDefaults.defaultAgent,
|
settingsDefaultAgent: openChamberDefaults.defaultAgent,
|
||||||
settingsAutoCreateWorktree: openChamberDefaults.autoCreateWorktree ?? false,
|
settingsAutoCreateWorktree: openChamberDefaults.autoCreateWorktree ?? false,
|
||||||
settingsGitmojiEnabled: openChamberDefaults.gitmojiEnabled ?? false,
|
settingsGitmojiEnabled: openChamberDefaults.gitmojiEnabled ?? false,
|
||||||
settingsZenModel: openChamberDefaults.zenModel,
|
settingsZenModel: resolvedZenModel,
|
||||||
|
settingsGitProviderId: resolvedGitProviderId,
|
||||||
|
settingsGitModelId: resolvedGitModelId,
|
||||||
directoryScoped: {
|
directoryScoped: {
|
||||||
...state.directoryScoped,
|
...state.directoryScoped,
|
||||||
[directoryKey]: nextSnapshot,
|
[directoryKey]: nextSnapshot,
|
||||||
@@ -1045,6 +1170,37 @@ export const useConfigStore = create<ConfigStore>()(
|
|||||||
return nextState;
|
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) {
|
if (safeAgents.length === 0) {
|
||||||
set((state) => {
|
set((state) => {
|
||||||
const baseSnapshot: DirectoryScopedConfig = state.directoryScoped[directoryKey] ?? {
|
const baseSnapshot: DirectoryScopedConfig = state.directoryScoped[directoryKey] ?? {
|
||||||
@@ -1460,6 +1616,24 @@ export const useConfigStore = create<ConfigStore>()(
|
|||||||
set({ settingsZenModel: model });
|
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') => {
|
setVoiceProvider: (provider: 'browser' | 'openai' | 'say') => {
|
||||||
set({ voiceProvider: provider });
|
set({ voiceProvider: provider });
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
@@ -1670,6 +1844,8 @@ export const useConfigStore = create<ConfigStore>()(
|
|||||||
settingsAutoCreateWorktree: state.settingsAutoCreateWorktree,
|
settingsAutoCreateWorktree: state.settingsAutoCreateWorktree,
|
||||||
settingsGitmojiEnabled: state.settingsGitmojiEnabled,
|
settingsGitmojiEnabled: state.settingsGitmojiEnabled,
|
||||||
settingsZenModel: state.settingsZenModel,
|
settingsZenModel: state.settingsZenModel,
|
||||||
|
settingsGitProviderId: state.settingsGitProviderId,
|
||||||
|
settingsGitModelId: state.settingsGitModelId,
|
||||||
speechRate: state.speechRate,
|
speechRate: state.speechRate,
|
||||||
speechPitch: state.speechPitch,
|
speechPitch: state.speechPitch,
|
||||||
speechVolume: state.speechVolume,
|
speechVolume: state.speechVolume,
|
||||||
|
|||||||
+269
-38
@@ -304,28 +304,253 @@ const normalizeMergeMethod = (value: string): 'merge' | 'squash' | 'rebase' => {
|
|||||||
return 'merge';
|
return 'merge';
|
||||||
};
|
};
|
||||||
|
|
||||||
const extractZenOutputText = (value: unknown): string | null => {
|
const BRIDGE_ZEN_DEFAULT_MODEL = 'gpt-5-nano';
|
||||||
if (!value || typeof value !== 'object') return null;
|
const BRIDGE_GIT_GENERATION_TIMEOUT_MS = 2 * 60 * 1000;
|
||||||
const root = value as Record<string, unknown>;
|
const BRIDGE_GIT_GENERATION_POLL_INTERVAL_MS = 500;
|
||||||
const output = root.output;
|
let bridgeGitModelCatalogCache: Set<string> | null = null;
|
||||||
if (!Array.isArray(output)) return null;
|
let bridgeGitModelCatalogCacheAt = 0;
|
||||||
|
const BRIDGE_GIT_MODEL_CATALOG_CACHE_TTL_MS = 30 * 1000;
|
||||||
|
|
||||||
const messageItem = output.find((item) => {
|
const sleep = (ms: number) => new Promise<void>((resolve) => {
|
||||||
if (!item || typeof item !== 'object') return false;
|
setTimeout(resolve, ms);
|
||||||
return (item as Record<string, unknown>).type === 'message';
|
});
|
||||||
}) as Record<string, unknown> | undefined;
|
|
||||||
if (!messageItem) return null;
|
|
||||||
|
|
||||||
const content = messageItem.content;
|
const fetchBridgeGitModelCatalog = async (
|
||||||
if (!Array.isArray(content)) return null;
|
apiUrl: string,
|
||||||
|
authHeaders?: Record<string, string>
|
||||||
|
): Promise<Set<string>> => {
|
||||||
|
const now = Date.now();
|
||||||
|
if (bridgeGitModelCatalogCache && now - bridgeGitModelCatalogCacheAt < BRIDGE_GIT_MODEL_CATALOG_CACHE_TTL_MS) {
|
||||||
|
return bridgeGitModelCatalogCache;
|
||||||
|
}
|
||||||
|
|
||||||
const textItem = content.find((item) => {
|
const headers = authHeaders || {};
|
||||||
if (!item || typeof item !== 'object') return false;
|
const modelsUrl = new URL(`${apiUrl.replace(/\/+$/, '')}/model`);
|
||||||
return (item as Record<string, unknown>).type === 'output_text';
|
const response = await fetch(modelsUrl.toString(), {
|
||||||
}) as Record<string, unknown> | undefined;
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
...headers,
|
||||||
|
},
|
||||||
|
signal: AbortSignal.timeout(8_000),
|
||||||
|
});
|
||||||
|
|
||||||
const text = typeof textItem?.text === 'string' ? textItem.text.trim() : '';
|
if (!response.ok) {
|
||||||
return text || null;
|
throw new Error('Failed to fetch model catalog');
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = await response.json().catch(() => null) as unknown;
|
||||||
|
const refs = new Set<string>();
|
||||||
|
if (Array.isArray(payload)) {
|
||||||
|
for (const item of payload) {
|
||||||
|
if (!item || typeof item !== 'object') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const record = item as Record<string, unknown>;
|
||||||
|
const providerID = typeof record.providerID === 'string' ? record.providerID.trim() : '';
|
||||||
|
const modelID = typeof record.modelID === 'string' ? record.modelID.trim() : '';
|
||||||
|
if (providerID && modelID) {
|
||||||
|
refs.add(`${providerID}/${modelID}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bridgeGitModelCatalogCache = refs;
|
||||||
|
bridgeGitModelCatalogCacheAt = now;
|
||||||
|
return refs;
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolveBridgeGitGenerationModel = async (
|
||||||
|
payloadModel: { providerId?: string; modelId?: string; zenModel?: string },
|
||||||
|
settings: Record<string, unknown>,
|
||||||
|
apiUrl: string,
|
||||||
|
authHeaders?: Record<string, string>
|
||||||
|
): Promise<{ providerID: string; modelID: string }> => {
|
||||||
|
let catalog: Set<string> | null = null;
|
||||||
|
try {
|
||||||
|
catalog = await fetchBridgeGitModelCatalog(apiUrl, authHeaders);
|
||||||
|
} catch {
|
||||||
|
catalog = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasModel = (providerID: string, modelID: string): boolean => {
|
||||||
|
if (!catalog) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return catalog.has(`${providerID}/${modelID}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const requestProviderId = typeof payloadModel.providerId === 'string' ? payloadModel.providerId.trim() : '';
|
||||||
|
const requestModelId = typeof payloadModel.modelId === 'string' ? payloadModel.modelId.trim() : '';
|
||||||
|
if (requestProviderId && requestModelId && hasModel(requestProviderId, requestModelId)) {
|
||||||
|
return { providerID: requestProviderId, modelID: requestModelId };
|
||||||
|
}
|
||||||
|
|
||||||
|
const settingsProviderId = readStringField(settings, 'gitProviderId');
|
||||||
|
const settingsModelId = readStringField(settings, 'gitModelId');
|
||||||
|
if (settingsProviderId && settingsModelId && hasModel(settingsProviderId, settingsModelId)) {
|
||||||
|
return { providerID: settingsProviderId, modelID: settingsModelId };
|
||||||
|
}
|
||||||
|
|
||||||
|
const payloadZenModel = typeof payloadModel.zenModel === 'string' ? payloadModel.zenModel.trim() : '';
|
||||||
|
const settingsZenModel = readStringField(settings, 'zenModel');
|
||||||
|
return {
|
||||||
|
providerID: 'zen',
|
||||||
|
modelID: payloadZenModel || settingsZenModel || BRIDGE_ZEN_DEFAULT_MODEL,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const extractTextFromMessageParts = (parts: unknown): string => {
|
||||||
|
if (!Array.isArray(parts)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const textParts = parts
|
||||||
|
.filter((part) => {
|
||||||
|
if (!part || typeof part !== 'object') return false;
|
||||||
|
const record = part as Record<string, unknown>;
|
||||||
|
return record.type === 'text' && typeof record.text === 'string';
|
||||||
|
})
|
||||||
|
.map((part) => (part as Record<string, unknown>).text as string)
|
||||||
|
.map((text) => text.trim())
|
||||||
|
.filter((text) => text.length > 0);
|
||||||
|
|
||||||
|
return textParts.join('\n').trim();
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateBridgeTextWithSessionFlow = async ({
|
||||||
|
apiUrl,
|
||||||
|
directory,
|
||||||
|
prompt,
|
||||||
|
providerID,
|
||||||
|
modelID,
|
||||||
|
authHeaders,
|
||||||
|
}: {
|
||||||
|
apiUrl: string;
|
||||||
|
directory: string;
|
||||||
|
prompt: string;
|
||||||
|
providerID: string;
|
||||||
|
modelID: string;
|
||||||
|
authHeaders?: Record<string, string>;
|
||||||
|
}): Promise<string> => {
|
||||||
|
const headers = authHeaders || {};
|
||||||
|
const apiBase = apiUrl.replace(/\/+$/, '');
|
||||||
|
const deadlineAt = Date.now() + BRIDGE_GIT_GENERATION_TIMEOUT_MS;
|
||||||
|
const remainingMs = () => Math.max(1_000, deadlineAt - Date.now());
|
||||||
|
let sessionId: string | null = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const sessionUrl = new URL(`${apiBase}/session`);
|
||||||
|
if (directory) {
|
||||||
|
sessionUrl.searchParams.set('directory', directory);
|
||||||
|
}
|
||||||
|
|
||||||
|
const createResponse = await fetch(sessionUrl.toString(), {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...headers,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ title: 'Git Generation' }),
|
||||||
|
signal: AbortSignal.timeout(remainingMs()),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!createResponse.ok) {
|
||||||
|
throw new Error('Failed to create OpenCode session');
|
||||||
|
}
|
||||||
|
|
||||||
|
const session = await createResponse.json().catch(() => null) as unknown;
|
||||||
|
const sessionObj = session && typeof session === 'object' ? session as Record<string, unknown> : null;
|
||||||
|
const createdSessionId = sessionObj && typeof sessionObj.id === 'string' ? sessionObj.id : '';
|
||||||
|
if (!createdSessionId) {
|
||||||
|
throw new Error('Invalid session response');
|
||||||
|
}
|
||||||
|
sessionId = createdSessionId;
|
||||||
|
|
||||||
|
const promptUrl = new URL(`${apiBase}/session/${encodeURIComponent(sessionId)}/prompt_async`);
|
||||||
|
if (directory) {
|
||||||
|
promptUrl.searchParams.set('directory', directory);
|
||||||
|
}
|
||||||
|
|
||||||
|
const promptResponse = await fetch(promptUrl.toString(), {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...headers,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
model: {
|
||||||
|
providerID,
|
||||||
|
modelID,
|
||||||
|
},
|
||||||
|
parts: [{ type: 'text', text: prompt }],
|
||||||
|
}),
|
||||||
|
signal: AbortSignal.timeout(remainingMs()),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!promptResponse.ok) {
|
||||||
|
throw new Error('Failed to send prompt');
|
||||||
|
}
|
||||||
|
|
||||||
|
const messagesUrl = new URL(`${apiBase}/session/${encodeURIComponent(sessionId)}/message`);
|
||||||
|
if (directory) {
|
||||||
|
messagesUrl.searchParams.set('directory', directory);
|
||||||
|
}
|
||||||
|
messagesUrl.searchParams.set('limit', '10');
|
||||||
|
|
||||||
|
while (Date.now() < deadlineAt) {
|
||||||
|
await sleep(BRIDGE_GIT_GENERATION_POLL_INTERVAL_MS);
|
||||||
|
|
||||||
|
const messagesResponse = await fetch(messagesUrl.toString(), {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
...headers,
|
||||||
|
},
|
||||||
|
signal: AbortSignal.timeout(remainingMs()),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!messagesResponse.ok) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const messages = await messagesResponse.json().catch(() => null) as unknown;
|
||||||
|
if (!Array.isArray(messages)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = messages.length - 1; i >= 0; i--) {
|
||||||
|
const message = messages[i] as Record<string, unknown> | null;
|
||||||
|
if (!message || typeof message !== 'object') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const info = message.info as Record<string, unknown> | undefined;
|
||||||
|
if (info?.role !== 'assistant' || info?.finish !== 'stop') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = extractTextFromMessageParts(message.parts);
|
||||||
|
if (text) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('Timeout waiting for generation to complete');
|
||||||
|
} finally {
|
||||||
|
if (sessionId) {
|
||||||
|
const deleteUrl = new URL(`${apiBase}/session/${encodeURIComponent(sessionId)}`);
|
||||||
|
try {
|
||||||
|
await fetch(deleteUrl.toString(), {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers,
|
||||||
|
signal: AbortSignal.timeout(5_000),
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// ignore cleanup failures
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const parseJsonObjectSafe = (value: string): Record<string, unknown> | null => {
|
const parseJsonObjectSafe = (value: string): Record<string, unknown> | null => {
|
||||||
@@ -2920,10 +3145,14 @@ export async function handleBridgeMessage(message: BridgeRequest, ctx?: BridgeCo
|
|||||||
}
|
}
|
||||||
|
|
||||||
case 'api:git/pr-description': {
|
case 'api:git/pr-description': {
|
||||||
const { directory, base, head } = (payload || {}) as {
|
const { directory, base, head, context, providerId, modelId, zenModel: payloadZenModel } = (payload || {}) as {
|
||||||
directory?: string;
|
directory?: string;
|
||||||
base?: string;
|
base?: string;
|
||||||
head?: string;
|
head?: string;
|
||||||
|
context?: string;
|
||||||
|
providerId?: string;
|
||||||
|
modelId?: string;
|
||||||
|
zenModel?: string;
|
||||||
};
|
};
|
||||||
if (!directory) {
|
if (!directory) {
|
||||||
return { id, type, success: false, error: 'Directory is required' };
|
return { id, type, success: false, error: 'Directory is required' };
|
||||||
@@ -2961,31 +3190,33 @@ export async function handleBridgeMessage(message: BridgeRequest, ctx?: BridgeCo
|
|||||||
return { id, type, success: false, error: 'No diffs available for selected files' };
|
return { id, type, success: false, error: 'No diffs available for selected files' };
|
||||||
}
|
}
|
||||||
|
|
||||||
const prompt = `You are drafting a GitHub Pull Request title + description. Respond in JSON of the shape {"title": string, "body": string} (ONLY JSON in response, no markdown fences) with these rules:\n- title: concise, sentence case, <= 80 chars, no trailing punctuation, no commit-style prefixes (no "feat:", "fix:")\n- body: GitHub-flavored markdown with these sections in this order: Summary, Testing, Notes\n- Summary: 3-6 bullet points describing user-visible changes; avoid internal helper function names\n- Testing: bullet list ("- Not tested" allowed)\n- Notes: bullet list; include breaking/rollout notes only when relevant\n\nContext:\n- base branch: ${base}\n- head branch: ${head}\n\nDiff summary:\n${diffSummaries}`;
|
const prompt = `You are drafting a GitHub Pull Request title + description. Respond in JSON of the shape {"title": string, "body": string} (ONLY JSON in response, no markdown fences) with these rules:\n- title: concise, sentence case, <= 80 chars, no trailing punctuation, no commit-style prefixes (no "feat:", "fix:")\n- body: GitHub-flavored markdown with these sections in this order: Summary, Testing, Notes\n- Summary: 3-6 bullet points describing user-visible changes; avoid internal helper function names\n- Testing: bullet list ("- Not tested" allowed)\n- Notes: bullet list; include breaking/rollout notes only when relevant\n\nContext:\n- base branch: ${base}\n- head branch: ${head}${context?.trim() ? `\n- Additional context: ${context.trim()}` : ''}\n\nDiff summary:\n${diffSummaries}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const zenSettings = readSettings(ctx) as Record<string, unknown>;
|
const apiUrl = ctx?.manager?.getApiUrl();
|
||||||
const zenModelRaw = typeof zenSettings?.zenModel === 'string' ? (zenSettings.zenModel as string).trim() : '';
|
if (!apiUrl) {
|
||||||
const zenModel = zenModelRaw.length > 0 ? zenModelRaw : 'gpt-5-nano';
|
return { id, type, success: false, error: 'OpenCode API unavailable' };
|
||||||
const response = await fetch('https://opencode.ai/zen/v1/responses', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({
|
|
||||||
model: zenModel,
|
|
||||||
input: [{ role: 'user', content: prompt }],
|
|
||||||
max_output_tokens: 1200,
|
|
||||||
stream: false,
|
|
||||||
reasoning: { effort: 'low' },
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
if (!response.ok) {
|
|
||||||
return { id, type, success: false, error: 'Failed to generate PR description' };
|
|
||||||
}
|
}
|
||||||
const data = await response.json().catch(() => null) as unknown;
|
|
||||||
const raw = extractZenOutputText(data);
|
const settings = readSettings(ctx) as Record<string, unknown>;
|
||||||
|
const { providerID, modelID } = await resolveBridgeGitGenerationModel(
|
||||||
|
{ providerId, modelId, zenModel: payloadZenModel },
|
||||||
|
settings,
|
||||||
|
apiUrl,
|
||||||
|
ctx?.manager?.getOpenCodeAuthHeaders()
|
||||||
|
);
|
||||||
|
const raw = await generateBridgeTextWithSessionFlow({
|
||||||
|
apiUrl,
|
||||||
|
directory,
|
||||||
|
prompt,
|
||||||
|
providerID,
|
||||||
|
modelID,
|
||||||
|
authHeaders: ctx?.manager?.getOpenCodeAuthHeaders(),
|
||||||
|
});
|
||||||
if (!raw) {
|
if (!raw) {
|
||||||
return { id, type, success: false, error: 'No PR description returned by generator' };
|
return { id, type, success: false, error: 'No PR description returned by generator' };
|
||||||
}
|
}
|
||||||
|
|
||||||
const cleaned = String(raw)
|
const cleaned = String(raw)
|
||||||
.trim()
|
.trim()
|
||||||
.replace(/^```json\s*/i, '')
|
.replace(/^```json\s*/i, '')
|
||||||
|
|||||||
@@ -90,10 +90,15 @@ export const createVSCodeGitAPI = (): GitAPI => ({
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
generateCommitMessage: async (directory: string, files: string[]): Promise<{ message: GeneratedCommitMessage }> => {
|
generateCommitMessage: async (
|
||||||
|
directory: string,
|
||||||
|
files: string[],
|
||||||
|
options?: { zenModel?: string; providerId?: string; modelId?: string }
|
||||||
|
): Promise<{ message: GeneratedCommitMessage }> => {
|
||||||
// This requires AI integration - stubbed for now
|
// This requires AI integration - stubbed for now
|
||||||
void directory; // Unused for now
|
void directory; // Unused for now
|
||||||
void files; // Unused for now
|
void files; // Unused for now
|
||||||
|
void options; // Unused for now
|
||||||
return {
|
return {
|
||||||
message: {
|
message: {
|
||||||
subject: '',
|
subject: '',
|
||||||
@@ -104,12 +109,16 @@ export const createVSCodeGitAPI = (): GitAPI => ({
|
|||||||
|
|
||||||
generatePullRequestDescription: async (
|
generatePullRequestDescription: async (
|
||||||
directory: string,
|
directory: string,
|
||||||
payload: { base: string; head: string }
|
payload: { base: string; head: string; context?: string; zenModel?: string; providerId?: string; modelId?: string }
|
||||||
): Promise<GeneratedPullRequestDescription> => {
|
): Promise<GeneratedPullRequestDescription> => {
|
||||||
return sendBridgeMessage<GeneratedPullRequestDescription>('api:git/pr-description', {
|
return sendBridgeMessage<GeneratedPullRequestDescription>('api:git/pr-description', {
|
||||||
directory,
|
directory,
|
||||||
base: payload.base,
|
base: payload.base,
|
||||||
head: payload.head,
|
head: payload.head,
|
||||||
|
context: payload.context,
|
||||||
|
zenModel: payload.zenModel,
|
||||||
|
providerId: payload.providerId,
|
||||||
|
modelId: payload.modelId,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
+237
-58
@@ -604,6 +604,9 @@ let validatedZenFallback = null;
|
|||||||
let cachedZenModels = null;
|
let cachedZenModels = null;
|
||||||
let cachedZenModelsTimestamp = 0;
|
let cachedZenModelsTimestamp = 0;
|
||||||
const ZEN_MODELS_CACHE_TTL = 5 * 60 * 1000; // 5 minutes
|
const ZEN_MODELS_CACHE_TTL = 5 * 60 * 1000; // 5 minutes
|
||||||
|
let cachedGitModelCatalog = null;
|
||||||
|
let cachedGitModelCatalogTimestamp = 0;
|
||||||
|
const GIT_MODEL_CATALOG_CACHE_TTL = 30 * 1000;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch free models from the zen API with caching. Returns an array of
|
* Fetch free models from the zen API with caching. Returns an array of
|
||||||
@@ -660,6 +663,220 @@ const resolveZenModel = async (override) => {
|
|||||||
return validatedZenFallback || ZEN_DEFAULT_MODEL;
|
return validatedZenFallback || ZEN_DEFAULT_MODEL;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getGitModelCatalog = async () => {
|
||||||
|
const now = Date.now();
|
||||||
|
if (cachedGitModelCatalog && now - cachedGitModelCatalogTimestamp < GIT_MODEL_CATALOG_CACHE_TTL) {
|
||||||
|
return cachedGitModelCatalog;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(buildOpenCodeUrl('/model', ''), {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
...getOpenCodeAuthHeaders(),
|
||||||
|
},
|
||||||
|
signal: AbortSignal.timeout(8_000),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to fetch model catalog: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = await response.json().catch(() => null);
|
||||||
|
const modelRefs = new Set();
|
||||||
|
if (Array.isArray(payload)) {
|
||||||
|
for (const item of payload) {
|
||||||
|
if (!item || typeof item !== 'object') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const providerID = typeof item.providerID === 'string' ? item.providerID.trim() : '';
|
||||||
|
const modelID = typeof item.modelID === 'string' ? item.modelID.trim() : '';
|
||||||
|
if (providerID && modelID) {
|
||||||
|
modelRefs.add(`${providerID}/${modelID}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cachedGitModelCatalog = modelRefs;
|
||||||
|
cachedGitModelCatalogTimestamp = now;
|
||||||
|
return modelRefs;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve git generation model based on priority:
|
||||||
|
* 1) request providerId+modelId
|
||||||
|
* 2) saved settings gitProviderId+gitModelId
|
||||||
|
* 3) legacy zenModel from request/settings as zen/<model>
|
||||||
|
* 4) Zen default (validatedZenFallback || ZEN_DEFAULT_MODEL)
|
||||||
|
*/
|
||||||
|
const resolveGitModel = async (requestParams) => {
|
||||||
|
const { providerId, modelId, zenModel } = requestParams || {};
|
||||||
|
const requestProviderId = typeof providerId === 'string' ? providerId.trim() : '';
|
||||||
|
const requestModelId = typeof modelId === 'string' ? modelId.trim() : '';
|
||||||
|
|
||||||
|
let modelCatalog = null;
|
||||||
|
try {
|
||||||
|
modelCatalog = await getGitModelCatalog();
|
||||||
|
} catch {
|
||||||
|
modelCatalog = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasModel = (providerID, modelID) => {
|
||||||
|
if (!modelCatalog) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return modelCatalog.has(`${providerID}/${modelID}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (requestProviderId && requestModelId && hasModel(requestProviderId, requestModelId)) {
|
||||||
|
return { providerID: requestProviderId, modelID: requestModelId };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const settings = await readSettingsFromDisk();
|
||||||
|
const settingsProviderId = typeof settings?.gitProviderId === 'string' ? settings.gitProviderId.trim() : '';
|
||||||
|
const settingsModelId = typeof settings?.gitModelId === 'string' ? settings.gitModelId.trim() : '';
|
||||||
|
if (settingsProviderId && settingsModelId && hasModel(settingsProviderId, settingsModelId)) {
|
||||||
|
return { providerID: settingsProviderId, modelID: settingsModelId };
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
const fallbackZenModel = typeof zenModel === 'string' && zenModel.trim().length > 0
|
||||||
|
? zenModel.trim()
|
||||||
|
: (await resolveZenModel(zenModel));
|
||||||
|
|
||||||
|
return { providerID: 'zen', modelID: fallbackZenModel };
|
||||||
|
};
|
||||||
|
|
||||||
|
const GIT_GENERATION_TIMEOUT_MS = 2 * 60 * 1000;
|
||||||
|
const GIT_GENERATION_POLL_INTERVAL_MS = 500;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate text using OpenCode session flow:
|
||||||
|
* - Create short-lived session
|
||||||
|
* - POST prompt_async with model and text prompt
|
||||||
|
* - Poll session messages until final assistant response
|
||||||
|
* - Extract text from parts
|
||||||
|
* - Best-effort cleanup of temporary session
|
||||||
|
*/
|
||||||
|
const generateWithSessionFlow = async ({ prompt, providerID, modelID }) => {
|
||||||
|
const completionTimeout = createTimeoutSignal(GIT_GENERATION_TIMEOUT_MS);
|
||||||
|
let sessionId = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const createUrl = buildOpenCodeUrl('/session', '');
|
||||||
|
const createResponse = await fetch(createUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Accept: 'application/json',
|
||||||
|
...getOpenCodeAuthHeaders(),
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
title: 'Git Generation',
|
||||||
|
}),
|
||||||
|
signal: completionTimeout.signal,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!createResponse.ok) {
|
||||||
|
const errorBody = await createResponse.json().catch(() => ({}));
|
||||||
|
throw new Error(`Failed to create session: ${createResponse.status} ${JSON.stringify(errorBody)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sessionData = await createResponse.json();
|
||||||
|
sessionId = sessionData?.id;
|
||||||
|
if (!sessionId) {
|
||||||
|
throw new Error('Session created but no ID returned');
|
||||||
|
}
|
||||||
|
|
||||||
|
const promptUrl = buildOpenCodeUrl(`/session/${encodeURIComponent(sessionId)}/prompt_async`, '');
|
||||||
|
const promptResponse = await fetch(promptUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Accept: 'application/json',
|
||||||
|
...getOpenCodeAuthHeaders(),
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
model: { providerID, modelID },
|
||||||
|
parts: [{ type: 'text', text: prompt }],
|
||||||
|
}),
|
||||||
|
signal: completionTimeout.signal,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!promptResponse.ok) {
|
||||||
|
const errorBody = await promptResponse.json().catch(() => ({}));
|
||||||
|
throw new Error(`Failed to send prompt: ${promptResponse.status} ${JSON.stringify(errorBody)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const messagesUrl = buildOpenCodeUrl(`/session/${encodeURIComponent(sessionId)}/message`, '');
|
||||||
|
let lastAssistantText = '';
|
||||||
|
let pollingAttempts = 0;
|
||||||
|
const maxPollingAttempts = Math.ceil(GIT_GENERATION_TIMEOUT_MS / GIT_GENERATION_POLL_INTERVAL_MS);
|
||||||
|
|
||||||
|
while (pollingAttempts < maxPollingAttempts) {
|
||||||
|
pollingAttempts++;
|
||||||
|
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, GIT_GENERATION_POLL_INTERVAL_MS));
|
||||||
|
|
||||||
|
const messagesResponse = await fetch(`${messagesUrl}?limit=10`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
...getOpenCodeAuthHeaders(),
|
||||||
|
},
|
||||||
|
signal: completionTimeout.signal,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!messagesResponse.ok) {
|
||||||
|
console.warn(`Session messages poll failed: ${messagesResponse.status}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const messages = await messagesResponse.json().catch(() => null);
|
||||||
|
if (!Array.isArray(messages)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = messages.length - 1; i >= 0; i--) {
|
||||||
|
const msg = messages[i];
|
||||||
|
if (msg?.info?.role === 'assistant' && msg?.info?.finish === 'stop') {
|
||||||
|
if (Array.isArray(msg.parts)) {
|
||||||
|
const textParts = msg.parts
|
||||||
|
.filter((p) => p?.type === 'text' && typeof p?.text === 'string')
|
||||||
|
.map((p) => p.text)
|
||||||
|
.filter(Boolean);
|
||||||
|
if (textParts.length > 0) {
|
||||||
|
return textParts.join('\n').trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('Timeout waiting for generation to complete');
|
||||||
|
} finally {
|
||||||
|
completionTimeout.cleanup();
|
||||||
|
|
||||||
|
if (sessionId) {
|
||||||
|
try {
|
||||||
|
const deleteUrl = buildOpenCodeUrl(`/session/${encodeURIComponent(sessionId)}`, '');
|
||||||
|
await fetch(deleteUrl, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: getOpenCodeAuthHeaders(),
|
||||||
|
signal: AbortSignal.timeout(5000),
|
||||||
|
}).catch((err) => {
|
||||||
|
console.warn('Failed to cleanup temporary session:', err?.message || err);
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('Failed to cleanup temporary session:', err?.message || err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const summarizeText = async (text, targetLength, zenModel) => {
|
const summarizeText = async (text, targetLength, zenModel) => {
|
||||||
if (!text || typeof text !== 'string' || text.trim().length === 0) return text;
|
if (!text || typeof text !== 'string' || text.trim().length === 0) return text;
|
||||||
|
|
||||||
@@ -1534,6 +1751,14 @@ const sanitizeSettingsUpdate = (payload) => {
|
|||||||
const trimmed = candidate.zenModel.trim();
|
const trimmed = candidate.zenModel.trim();
|
||||||
result.zenModel = trimmed.length > 0 ? trimmed : undefined;
|
result.zenModel = trimmed.length > 0 ? trimmed : undefined;
|
||||||
}
|
}
|
||||||
|
if (typeof candidate.gitProviderId === 'string') {
|
||||||
|
const trimmed = candidate.gitProviderId.trim();
|
||||||
|
result.gitProviderId = trimmed.length > 0 ? trimmed : undefined;
|
||||||
|
}
|
||||||
|
if (typeof candidate.gitModelId === 'string') {
|
||||||
|
const trimmed = candidate.gitModelId.trim();
|
||||||
|
result.gitModelId = trimmed.length > 0 ? trimmed : undefined;
|
||||||
|
}
|
||||||
if (typeof candidate.toolCallExpansion === 'string') {
|
if (typeof candidate.toolCallExpansion === 'string') {
|
||||||
const mode = candidate.toolCallExpansion.trim();
|
const mode = candidate.toolCallExpansion.trim();
|
||||||
if (mode === 'collapsed' || mode === 'activity' || mode === 'detailed') {
|
if (mode === 'collapsed' || mode === 'activity' || mode === 'detailed') {
|
||||||
@@ -9713,37 +9938,13 @@ highlights:
|
|||||||
Diff summary (may be truncated):
|
Diff summary (may be truncated):
|
||||||
${diffSummaries}`;
|
${diffSummaries}`;
|
||||||
|
|
||||||
const model = await resolveZenModel(typeof req.body?.zenModel === 'string' ? req.body.zenModel : undefined);
|
const { providerID, modelID } = await resolveGitModel({
|
||||||
|
providerId: req.body?.providerId,
|
||||||
|
modelId: req.body?.modelId,
|
||||||
|
zenModel: req.body?.zenModel,
|
||||||
|
});
|
||||||
|
|
||||||
const completionTimeout = createTimeoutSignal(LONG_REQUEST_TIMEOUT_MS);
|
const raw = await generateWithSessionFlow({ prompt, providerID, modelID });
|
||||||
let response;
|
|
||||||
try {
|
|
||||||
response = await fetch('https://opencode.ai/zen/v1/responses', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({
|
|
||||||
model,
|
|
||||||
input: [{ role: 'user', content: prompt }],
|
|
||||||
max_output_tokens: 1000,
|
|
||||||
stream: false,
|
|
||||||
reasoning: {
|
|
||||||
effort: 'low'
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
signal: completionTimeout.signal,
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
completionTimeout.cleanup();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorBody = await response.json().catch(() => ({}));
|
|
||||||
console.error('Commit message generation failed:', errorBody);
|
|
||||||
return res.status(502).json({ error: 'Failed to generate commit message' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
const raw = data?.output?.find((item) => item?.type === 'message')?.content?.find((item) => item?.type === 'output_text')?.text?.trim();
|
|
||||||
|
|
||||||
if (!raw) {
|
if (!raw) {
|
||||||
return res.status(502).json({ error: 'No commit message returned by generator' });
|
return res.status(502).json({ error: 'No commit message returned by generator' });
|
||||||
@@ -9826,35 +10027,13 @@ Context:
|
|||||||
|
|
||||||
prompt += `\n\nDiff summary:\n${diffSummaries}`;
|
prompt += `\n\nDiff summary:\n${diffSummaries}`;
|
||||||
|
|
||||||
const model = await resolveZenModel(typeof req.body?.zenModel === 'string' ? req.body.zenModel : undefined);
|
const { providerID, modelID } = await resolveGitModel({
|
||||||
|
providerId: req.body?.providerId,
|
||||||
|
modelId: req.body?.modelId,
|
||||||
|
zenModel: req.body?.zenModel,
|
||||||
|
});
|
||||||
|
|
||||||
const completionTimeout = createTimeoutSignal(LONG_REQUEST_TIMEOUT_MS);
|
const raw = await generateWithSessionFlow({ prompt, providerID, modelID });
|
||||||
let response;
|
|
||||||
try {
|
|
||||||
response = await fetch('https://opencode.ai/zen/v1/responses', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({
|
|
||||||
model,
|
|
||||||
input: [{ role: 'user', content: prompt }],
|
|
||||||
max_output_tokens: 1200,
|
|
||||||
stream: false,
|
|
||||||
reasoning: { effort: 'low' },
|
|
||||||
}),
|
|
||||||
signal: completionTimeout.signal,
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
completionTimeout.cleanup();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorBody = await response.json().catch(() => ({}));
|
|
||||||
console.error('PR description generation failed:', errorBody);
|
|
||||||
return res.status(502).json({ error: 'Failed to generate PR description' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
const raw = data?.output?.find((item) => item?.type === 'message')?.content?.find((item) => item?.type === 'output_text')?.text?.trim();
|
|
||||||
if (!raw) {
|
if (!raw) {
|
||||||
return res.status(502).json({ error: 'No PR description returned by generator' });
|
return res.status(502).json({ error: 'No PR description returned by generator' });
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user