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:
Cursor Agent
2026-08-04 12:22:54 +00:00
co-authored by Serhii Dziupin
parent abb396e080
commit 35f17e9e96
17 changed files with 49 additions and 60 deletions
@@ -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
-3
View File
@@ -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',
-3
View File
@@ -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',
-3
View File
@@ -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",
-3
View File
@@ -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 nest pas connecté',
'walkthrough.blocked.noProviderLogin.description': '{model} nécessite une connexion à son fournisseur. Connectez-vous, ou choisissez un modèle dun 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 dun fournisseur que vous utilisez déjà.',
'walkthrough.blocked.emptyDiff.title': 'Rien à examiner',
'walkthrough.blocked.emptyDiff.description': 'Il ny a encore aucune modification dans cette portée.',
'walkthrough.blocked.contextTooSmall.title': 'Ce diff est trop volumineux pour le modèle actuel',
-3
View File
@@ -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': 'この差分は現在のモデルには大きすぎます',
-3
View File
@@ -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는 현재 모델에 너무 큽니다',
-3
View File
@@ -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",
-3
View File
@@ -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': '目前模型無法容納這份差異',
+2
View File
@@ -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
+3 -2
View File
@@ -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 () => {