fix(walkthrough): hide unauthenticated models and disable Generate
Do not present a provider without a login as the selected walkthrough model, and grey out Generate when readiness is false instead of showing a login-error blocker or raw auth banner. Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
This commit is contained in:
co-authored by
Serhii Dziupin
parent
abb396e080
commit
35f17e9e96
@@ -41,8 +41,7 @@ export const WalkthroughBlocker = ({
|
||||
// Settings.
|
||||
const canChooseModel = reason === 'context-too-small'
|
||||
|| reason === 'structured-output-unsupported'
|
||||
|| reason === 'output-exhausted'
|
||||
|| reason === 'no-provider-login';
|
||||
|| reason === 'output-exhausted';
|
||||
|
||||
useEffect(() => {
|
||||
if (!canChooseModel || providers !== undefined) return;
|
||||
@@ -101,11 +100,6 @@ export const WalkthroughBlocker = ({
|
||||
|
||||
const description = () => {
|
||||
if (reason === 'no-model') return t('walkthrough.blocked.noModel.description');
|
||||
if (reason === 'no-provider-login') {
|
||||
return label
|
||||
? t('walkthrough.blocked.noProviderLogin.description', { model: label })
|
||||
: t('walkthrough.blocked.noProviderLogin.descriptionUnknownModel');
|
||||
}
|
||||
if (reason === 'empty-diff') return t('walkthrough.blocked.emptyDiff.description');
|
||||
if (reason === 'only-generated') return t('walkthrough.blocked.onlyGenerated.description');
|
||||
if (reason === 'output-exhausted') {
|
||||
@@ -129,7 +123,6 @@ export const WalkthroughBlocker = ({
|
||||
|
||||
const title = () => {
|
||||
if (reason === 'no-model') return t('walkthrough.blocked.noModel.title');
|
||||
if (reason === 'no-provider-login') return t('walkthrough.blocked.noProviderLogin.title');
|
||||
if (reason === 'empty-diff') return t('walkthrough.blocked.emptyDiff.title');
|
||||
if (reason === 'only-generated') return t('walkthrough.blocked.onlyGenerated.title');
|
||||
if (reason === 'output-exhausted') return t('walkthrough.blocked.outputExhausted.title');
|
||||
|
||||
@@ -323,15 +323,34 @@ export const WalkthroughView = ({ directory }: WalkthroughViewProps) => {
|
||||
// Explicit pick first, then the model that actually produced what is on
|
||||
// screen, then whatever settings resolve to. The middle step is what makes
|
||||
// reopening a review show the model behind it rather than the default.
|
||||
const activeModel = selectedModel
|
||||
?? (entry.result?.model ? `${entry.result.model.providerID}/${entry.result.model.modelID}` : undefined)
|
||||
?? (entry.readiness?.model ? `${entry.readiness.model.providerID}/${entry.readiness.model.modelID}` : undefined);
|
||||
const [activeProviderId, ...activeModelParts] = (activeModel ?? '').split('/');
|
||||
const activeModelId = activeModelParts.join('/');
|
||||
|
||||
// Never present a provider without a usable login as the current selection —
|
||||
// the picker already hides them from the menu; showing one as selected was
|
||||
// the whole "why say so?" failure mode.
|
||||
const modelsMetadata = useConfigStore((state) => state.modelsMetadata);
|
||||
const [modelProviders, setModelProviders] = useState<string[] | undefined>(undefined);
|
||||
|
||||
const providerIsAuthenticated = (providerId: string | undefined) => {
|
||||
if (!providerId) return false;
|
||||
if (modelProviders === undefined) return true;
|
||||
return modelProviders.includes(providerId);
|
||||
};
|
||||
const readinessModelRef = entry.readiness?.model
|
||||
&& entry.readiness.model.hasLogin !== false
|
||||
&& providerIsAuthenticated(entry.readiness.model.providerID)
|
||||
? `${entry.readiness.model.providerID}/${entry.readiness.model.modelID}`
|
||||
: undefined;
|
||||
const resultModelRef = entry.result?.model
|
||||
&& providerIsAuthenticated(entry.result.model.providerID)
|
||||
? `${entry.result.model.providerID}/${entry.result.model.modelID}`
|
||||
: undefined;
|
||||
const selectedModelUsable = selectedModel
|
||||
&& providerIsAuthenticated(selectedModel.split('/')[0])
|
||||
? selectedModel
|
||||
: undefined;
|
||||
const activeModel = selectedModelUsable ?? resultModelRef ?? readinessModelRef;
|
||||
const [activeProviderId, ...activeModelParts] = (activeModel ?? '').split('/');
|
||||
const activeModelId = activeModelParts.join('/');
|
||||
|
||||
useEffect(() => {
|
||||
if (modelProviders !== undefined) return;
|
||||
let cancelled = false;
|
||||
@@ -403,15 +422,17 @@ export const WalkthroughView = ({ directory }: WalkthroughViewProps) => {
|
||||
|
||||
const showStages = startedFromEmptyRef.current
|
||||
&& (entry.status === 'generating' || stageProgress.holding);
|
||||
// Auth/login gaps are not a full-panel blocker: hide the unusable model and
|
||||
// disable Generate instead of explaining a raw provider error.
|
||||
const blockedReason = entry.error?.code === 'context-too-small'
|
||||
|| entry.error?.code === 'structured-output-unsupported'
|
||||
|| entry.error?.code === 'no-model'
|
||||
|| entry.error?.code === 'no-provider-login'
|
||||
|| entry.error?.code === 'empty-diff'
|
||||
|| entry.error?.code === 'only-generated'
|
||||
|| entry.error?.code === 'output-exhausted'
|
||||
? entry.error.code
|
||||
: entry.readiness && !entry.readiness.ready && !view
|
||||
&& entry.readiness.reason !== 'no-provider-login'
|
||||
? entry.readiness.reason
|
||||
: undefined;
|
||||
|
||||
@@ -421,11 +442,16 @@ export const WalkthroughView = ({ directory }: WalkthroughViewProps) => {
|
||||
const blockedRequiredChars = entry.error?.requiredChars ?? entry.readiness?.requiredChars;
|
||||
const blockedAvailableChars = entry.error?.availableChars ?? entry.readiness?.availableChars;
|
||||
|
||||
// Not ready means Generate must not look actionable — including when the
|
||||
// resolved model has no login (reason no-provider-login).
|
||||
const generateDisabled = Boolean(entry.readiness && !entry.readiness.ready);
|
||||
|
||||
const handleGenerate = useCallback(
|
||||
(force: boolean) => {
|
||||
if (generateDisabled) return;
|
||||
void generate(directory, source, { force, language: activeLanguage });
|
||||
},
|
||||
[activeLanguage, directory, generate, source]
|
||||
[activeLanguage, directory, generate, generateDisabled, source]
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -594,6 +620,7 @@ export const WalkthroughView = ({ directory }: WalkthroughViewProps) => {
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className={WALKTHROUGH_ACTION_CLASS}
|
||||
disabled={generateDisabled}
|
||||
aria-label={compactHeader
|
||||
? (view ? t('walkthrough.action.regenerate') : t('walkthrough.action.generate'))
|
||||
: undefined}
|
||||
@@ -647,6 +674,7 @@ export const WalkthroughView = ({ directory }: WalkthroughViewProps) => {
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
className="ml-auto"
|
||||
disabled={generateDisabled}
|
||||
// Not forced: if an entry for this exact request existed the banner
|
||||
// would not be here, and a forced run would refuse the cache it may
|
||||
// find on the way.
|
||||
@@ -678,7 +706,7 @@ export const WalkthroughView = ({ directory }: WalkthroughViewProps) => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{entry.error && !blockedReason && (
|
||||
{entry.error && !blockedReason && entry.error.code !== 'no-provider-login' && (
|
||||
<div className="flex shrink-0 items-start gap-2 border-b border-border/60 bg-status-error/10 px-3 py-2">
|
||||
<Icon name="error-warning" className="mt-0.5 size-4 shrink-0 text-status-error" />
|
||||
{/* Provider errors arrive as raw JSON bodies. Show a readable amount
|
||||
|
||||
@@ -2866,9 +2866,6 @@ export const dict = {
|
||||
'walkthrough.importance.context': 'Kontext',
|
||||
'walkthrough.blocked.noModel.title': 'Kein Modell ausgewählt',
|
||||
'walkthrough.blocked.noModel.description': 'Wählen Sie zuerst ein Modell aus.',
|
||||
'walkthrough.blocked.noProviderLogin.title': 'Dieser Anbieter ist nicht angemeldet',
|
||||
'walkthrough.blocked.noProviderLogin.description': '{model} braucht eine Anmeldung bei seinem Anbieter. Melde dich an oder wähle ein Modell eines Anbieters, den du bereits nutzt.',
|
||||
'walkthrough.blocked.noProviderLogin.descriptionUnknownModel': 'Das ausgewählte Modell braucht eine Anmeldung bei seinem Anbieter. Melde dich an oder wähle ein Modell eines Anbieters, den du bereits nutzt.',
|
||||
'walkthrough.blocked.emptyDiff.title': 'Kein Diff vorhanden',
|
||||
'walkthrough.blocked.emptyDiff.description': 'Es gibt keine Änderungen, die zusammengefasst werden können.',
|
||||
'walkthrough.blocked.contextTooSmall.title': 'Kontext zu klein',
|
||||
|
||||
@@ -1147,9 +1147,6 @@ export const dict = {
|
||||
'walkthrough.importance.context': 'Context',
|
||||
'walkthrough.blocked.noModel.title': 'No small model available',
|
||||
'walkthrough.blocked.noModel.description': 'Sign in to a model provider to generate a review.',
|
||||
'walkthrough.blocked.noProviderLogin.title': 'This provider is not signed in',
|
||||
'walkthrough.blocked.noProviderLogin.description': '{model} needs a login for its provider. Sign in, or choose a model from a provider you already use.',
|
||||
'walkthrough.blocked.noProviderLogin.descriptionUnknownModel': 'The selected model needs a login for its provider. Sign in, or choose a model from a provider you already use.',
|
||||
'walkthrough.blocked.emptyDiff.title': 'Nothing to review',
|
||||
'walkthrough.blocked.emptyDiff.description': 'There are no changes in this scope yet.',
|
||||
'walkthrough.blocked.contextTooSmall.title': 'This diff is too large for the current model',
|
||||
|
||||
@@ -1148,9 +1148,6 @@ export const dict: Record<I18nKey, string> = {
|
||||
"walkthrough.importance.context": "Contexto",
|
||||
"walkthrough.blocked.noModel.title": "No hay ningún modelo pequeño disponible",
|
||||
"walkthrough.blocked.noModel.description": "Inicia sesión en un proveedor de modelos para generar una revisión.",
|
||||
"walkthrough.blocked.noProviderLogin.title": "Este proveedor no tiene sesión iniciada",
|
||||
"walkthrough.blocked.noProviderLogin.description": "{model} necesita un inicio de sesión en su proveedor. Inicia sesión o elige un modelo de un proveedor que ya uses.",
|
||||
"walkthrough.blocked.noProviderLogin.descriptionUnknownModel": "El modelo seleccionado necesita un inicio de sesión en su proveedor. Inicia sesión o elige un modelo de un proveedor que ya uses.",
|
||||
"walkthrough.blocked.emptyDiff.title": "Nada que revisar",
|
||||
"walkthrough.blocked.emptyDiff.description": "Todavía no hay cambios en este ámbito.",
|
||||
"walkthrough.blocked.contextTooSmall.title": "Este diff es demasiado grande para el modelo actual",
|
||||
|
||||
@@ -972,9 +972,6 @@ export const dict = {
|
||||
'walkthrough.importance.context': 'Contexte',
|
||||
'walkthrough.blocked.noModel.title': 'Aucun petit modèle disponible',
|
||||
'walkthrough.blocked.noModel.description': 'Connectez-vous à un fournisseur de modèles pour générer une revue.',
|
||||
'walkthrough.blocked.noProviderLogin.title': 'Ce fournisseur n’est pas connecté',
|
||||
'walkthrough.blocked.noProviderLogin.description': '{model} nécessite une connexion à son fournisseur. Connectez-vous, ou choisissez un modèle d’un fournisseur que vous utilisez déjà.',
|
||||
'walkthrough.blocked.noProviderLogin.descriptionUnknownModel': 'Le modèle sélectionné nécessite une connexion à son fournisseur. Connectez-vous, ou choisissez un modèle d’un fournisseur que vous utilisez déjà.',
|
||||
'walkthrough.blocked.emptyDiff.title': 'Rien à examiner',
|
||||
'walkthrough.blocked.emptyDiff.description': 'Il n’y a encore aucune modification dans cette portée.',
|
||||
'walkthrough.blocked.contextTooSmall.title': 'Ce diff est trop volumineux pour le modèle actuel',
|
||||
|
||||
@@ -1144,9 +1144,6 @@ export const dict: Record<I18nKey, string> = {
|
||||
'walkthrough.importance.context': '補足',
|
||||
'walkthrough.blocked.noModel.title': '利用できるスモールモデルがありません',
|
||||
'walkthrough.blocked.noModel.description': 'レビューを生成するにはモデルプロバイダーにサインインしてください。',
|
||||
'walkthrough.blocked.noProviderLogin.title': 'このプロバイダーにはサインインしていません',
|
||||
'walkthrough.blocked.noProviderLogin.description': '{model} にはプロバイダーへのログインが必要です。サインインするか、すでに使っているプロバイダーのモデルを選んでください。',
|
||||
'walkthrough.blocked.noProviderLogin.descriptionUnknownModel': '選択したモデルにはプロバイダーへのログインが必要です。サインインするか、すでに使っているプロバイダーのモデルを選んでください。',
|
||||
'walkthrough.blocked.emptyDiff.title': 'レビュー対象がありません',
|
||||
'walkthrough.blocked.emptyDiff.description': 'この範囲にはまだ変更がありません。',
|
||||
'walkthrough.blocked.contextTooSmall.title': 'この差分は現在のモデルには大きすぎます',
|
||||
|
||||
@@ -1148,9 +1148,6 @@ export const dict: Record<I18nKey, string> = {
|
||||
'walkthrough.importance.context': '참고',
|
||||
'walkthrough.blocked.noModel.title': '사용할 수 있는 스몰 모델이 없습니다',
|
||||
'walkthrough.blocked.noModel.description': '리뷰를 생성하려면 모델 제공자에 로그인하세요.',
|
||||
'walkthrough.blocked.noProviderLogin.title': '이 제공자에 로그인되어 있지 않습니다',
|
||||
'walkthrough.blocked.noProviderLogin.description': '{model}을(를) 쓰려면 해당 제공자에 로그인해야 합니다. 로그인하거나, 이미 사용 중인 제공자의 모델을 선택하세요.',
|
||||
'walkthrough.blocked.noProviderLogin.descriptionUnknownModel': '선택한 모델을 쓰려면 해당 제공자에 로그인해야 합니다. 로그인하거나, 이미 사용 중인 제공자의 모델을 선택하세요.',
|
||||
'walkthrough.blocked.emptyDiff.title': '리뷰할 내용이 없습니다',
|
||||
'walkthrough.blocked.emptyDiff.description': '이 범위에는 아직 변경 사항이 없습니다.',
|
||||
'walkthrough.blocked.contextTooSmall.title': '이 diff는 현재 모델에 너무 큽니다',
|
||||
|
||||
@@ -1460,9 +1460,6 @@ export const dict: Record<I18nKey, string> = {
|
||||
'walkthrough.importance.context': 'Kontekst',
|
||||
'walkthrough.blocked.noModel.title': 'Brak dostępnego małego modelu',
|
||||
'walkthrough.blocked.noModel.description': 'Zaloguj się u dostawcy modeli, aby wygenerować przegląd.',
|
||||
'walkthrough.blocked.noProviderLogin.title': 'Ten dostawca nie jest zalogowany',
|
||||
'walkthrough.blocked.noProviderLogin.description': '{model} wymaga logowania u swojego dostawcy. Zaloguj się albo wybierz model u dostawcy, którego już używasz.',
|
||||
'walkthrough.blocked.noProviderLogin.descriptionUnknownModel': 'Wybrany model wymaga logowania u swojego dostawcy. Zaloguj się albo wybierz model u dostawcy, którego już używasz.',
|
||||
'walkthrough.blocked.emptyDiff.title': 'Nie ma czego przeglądać',
|
||||
'walkthrough.blocked.emptyDiff.description': 'W tym zakresie nie ma jeszcze zmian.',
|
||||
'walkthrough.blocked.contextTooSmall.title': 'Te różnice są za duże dla bieżącego modelu',
|
||||
|
||||
@@ -1148,9 +1148,6 @@ export const dict: Record<I18nKey, string> = {
|
||||
"walkthrough.importance.context": "Contexto",
|
||||
"walkthrough.blocked.noModel.title": "Nenhum modelo pequeno disponível",
|
||||
"walkthrough.blocked.noModel.description": "Entre em um provedor de modelos para gerar uma revisão.",
|
||||
"walkthrough.blocked.noProviderLogin.title": "Este provedor não está conectado",
|
||||
"walkthrough.blocked.noProviderLogin.description": "{model} precisa de login no provedor. Entre na conta ou escolha um modelo de um provedor que você já usa.",
|
||||
"walkthrough.blocked.noProviderLogin.descriptionUnknownModel": "O modelo selecionado precisa de login no provedor. Entre na conta ou escolha um modelo de um provedor que você já usa.",
|
||||
"walkthrough.blocked.emptyDiff.title": "Nada para revisar",
|
||||
"walkthrough.blocked.emptyDiff.description": "Ainda não há mudanças neste escopo.",
|
||||
"walkthrough.blocked.contextTooSmall.title": "Este diff é grande demais para o modelo atual",
|
||||
|
||||
@@ -1148,9 +1148,6 @@ export const dict: Record<I18nKey, string> = {
|
||||
"walkthrough.importance.context": "Контекст",
|
||||
"walkthrough.blocked.noModel.title": "Немає доступної small model",
|
||||
"walkthrough.blocked.noModel.description": "Увійдіть до провайдера моделей, щоб створити розбір.",
|
||||
"walkthrough.blocked.noProviderLogin.title": "У цей провайдер не ввійшли",
|
||||
"walkthrough.blocked.noProviderLogin.description": "{model} потребує входу в його провайдер. Увійдіть або виберіть модель у провайдера, яким ви вже користуєтесь.",
|
||||
"walkthrough.blocked.noProviderLogin.descriptionUnknownModel": "Вибрана модель потребує входу в її провайдер. Увійдіть або виберіть модель у провайдера, яким ви вже користуєтесь.",
|
||||
"walkthrough.blocked.emptyDiff.title": "Немає що оглядати",
|
||||
"walkthrough.blocked.emptyDiff.description": "У цій області поки що немає змін.",
|
||||
"walkthrough.blocked.contextTooSmall.title": "Цей diff завеликий для поточної моделі",
|
||||
|
||||
@@ -1148,9 +1148,6 @@ export const dict: Record<I18nKey, string> = {
|
||||
'walkthrough.importance.context': '背景',
|
||||
'walkthrough.blocked.noModel.title': '没有可用的小模型',
|
||||
'walkthrough.blocked.noModel.description': '请登录模型提供方后再生成评审。',
|
||||
'walkthrough.blocked.noProviderLogin.title': '尚未登录此提供方',
|
||||
'walkthrough.blocked.noProviderLogin.description': '{model} 需要登录其提供方。请先登录,或改选你已在使用的提供方中的模型。',
|
||||
'walkthrough.blocked.noProviderLogin.descriptionUnknownModel': '所选模型需要登录其提供方。请先登录,或改选你已在使用的提供方中的模型。',
|
||||
'walkthrough.blocked.emptyDiff.title': '没有可评审的内容',
|
||||
'walkthrough.blocked.emptyDiff.description': '该范围内暂无改动。',
|
||||
'walkthrough.blocked.contextTooSmall.title': '当前模型无法容纳这份差异',
|
||||
|
||||
@@ -1160,9 +1160,6 @@ export const dict: Record<I18nKey, string> = {
|
||||
'walkthrough.importance.context': '背景',
|
||||
'walkthrough.blocked.noModel.title': '沒有可用的小模型',
|
||||
'walkthrough.blocked.noModel.description': '請先登入模型供應商再產生審閱。',
|
||||
'walkthrough.blocked.noProviderLogin.title': '尚未登入此供應商',
|
||||
'walkthrough.blocked.noProviderLogin.description': '{model} 需要登入其供應商。請先登入,或改選你已在使用的供應商中的模型。',
|
||||
'walkthrough.blocked.noProviderLogin.descriptionUnknownModel': '所選模型需要登入其供應商。請先登入,或改選你已在使用的供應商中的模型。',
|
||||
'walkthrough.blocked.emptyDiff.title': '沒有可審閱的內容',
|
||||
'walkthrough.blocked.emptyDiff.description': '此範圍目前沒有變更。',
|
||||
'walkthrough.blocked.contextTooSmall.title': '目前模型無法容納這份差異',
|
||||
|
||||
@@ -105,6 +105,8 @@ export interface WalkthroughReadiness {
|
||||
inputCharBudget?: number;
|
||||
contextTokens?: number;
|
||||
structuredOutput?: boolean | null;
|
||||
/** False when the resolved provider has no usable OpenCode login. */
|
||||
hasLogin?: boolean;
|
||||
};
|
||||
requiredChars?: number;
|
||||
availableChars?: number;
|
||||
|
||||
@@ -121,9 +121,10 @@ silently changes the model behind commit messages.
|
||||
A settings or `opencode.json` `small_model` override can still name a provider
|
||||
with no usable login (neither `auth.json` nor `provider.<id>.options.apiKey`).
|
||||
`describeSmallModel` reports that as `hasLogin: false`, readiness refuses with
|
||||
`code: 'no-provider-login'`, and generation maps the same code to HTTP 401 —
|
||||
so the panel shows a blocker with a model picker instead of looking ready and
|
||||
then dumping the raw `No OpenCode login found for provider "…"` string.
|
||||
`reason: 'no-provider-login'` and omits the unusable model so the panel cannot
|
||||
present it as selected, and generation maps the same code to HTTP 401. The UI
|
||||
disables Generate and keeps the picker on authenticated providers only — it does
|
||||
not surface a raw auth error or a special login blocker for this case.
|
||||
|
||||
## Output language
|
||||
|
||||
|
||||
@@ -334,9 +334,10 @@ function computeReadiness({ model, digest, files, fileCount, hunkCount, generate
|
||||
}
|
||||
|
||||
// A resolved override/config model can still have no usable login. Refuse up
|
||||
// front so the panel does not look ready and then dump a raw auth error.
|
||||
// front and omit the model — offering an unauthenticated selection in the
|
||||
// picker is what made the old raw auth error feel like a product bug.
|
||||
if (model.hasLogin === false) {
|
||||
return { ready: false, reason: 'no-provider-login', model };
|
||||
return { ready: false, reason: 'no-provider-login' };
|
||||
}
|
||||
|
||||
// Built with the same language the generation would use: the instruction is
|
||||
|
||||
@@ -90,11 +90,8 @@ describe('issue 2607 — walkthrough blocks unauthenticated providers', () => {
|
||||
|
||||
expect(result.readiness.ready).toBe(false);
|
||||
expect(result.readiness.reason).toBe('no-provider-login');
|
||||
expect(result.readiness.model).toMatchObject({
|
||||
providerID: 'deepseek',
|
||||
modelID: 'deepseek-v4-flash',
|
||||
hasLogin: false,
|
||||
});
|
||||
// Unusable models must not be offered as the current selection.
|
||||
expect(result.readiness.model).toBeUndefined();
|
||||
});
|
||||
|
||||
it('callSmallModel throws a structured no-provider-login error', async () => {
|
||||
|
||||
Reference in New Issue
Block a user