desktop AI fix: a claude CLI without a sign-in ("Not logged in · Please run /login", seen in a real headless run) shows 'not signed in' with the copy-for-AI fallback instead of a bare failure; the flags were checked against a real claude run (it edited the file)

This commit is contained in:
CRTkafa 2026-10-15 06:28:52 +08:00
parent c95f2af8ff
commit 5ed787a8ca
4 changed files with 19 additions and 1 deletions

View file

@ -453,6 +453,7 @@ export const en = {
'ai.dirty': 'Uncommitted files in the app folder ({count}): commit or stash them first, so Undo can restore exactly what the AI changes.', 'ai.dirty': 'Uncommitted files in the app folder ({count}): commit or stash them first, so Undo can restore exactly what the AI changes.',
'ai.noGit': 'The app folder is not in a git repository: Fix with AI needs git for the diff and Undo.', 'ai.noGit': 'The app folder is not in a git repository: Fix with AI needs git for the diff and Undo.',
'ai.cancelled': 'Cancelled', 'ai.cancelled': 'Cancelled',
'ai.signedOut': 'Claude Code is not signed in: run claude once in a terminal and sign in with /login, or copy the prompt for your AI agent',
'ai.timedOut': 'Stopped after 10 minutes', 'ai.timedOut': 'Stopped after 10 minutes',
'ai.noChanges': 'No files changed', 'ai.noChanges': 'No files changed',
'ai.changed': 'Changed files: {count}', 'ai.changed': 'Changed files: {count}',

View file

@ -455,6 +455,7 @@ export const tr: Record<keyof typeof en, string> = {
'ai.dirty': 'Uygulama klasöründe commit edilmemiş {count} dosya var: önce commit et ya da stash yap ki Geri al tam olarak yapay zekânın değiştirdiğini geri yüklesin.', 'ai.dirty': 'Uygulama klasöründe commit edilmemiş {count} dosya var: önce commit et ya da stash yap ki Geri al tam olarak yapay zekânın değiştirdiğini geri yüklesin.',
'ai.noGit': 'Uygulama klasörü bir git deposunda değil: Yapay zekâyla düzelt fark ve Geri al için git ister.', 'ai.noGit': 'Uygulama klasörü bir git deposunda değil: Yapay zekâyla düzelt fark ve Geri al için git ister.',
'ai.cancelled': 'İptal edildi', 'ai.cancelled': 'İptal edildi',
'ai.signedOut': 'Claude Code oturum açmamış: bir terminalde claude çalıştırıp /login ile giriş yapın ya da istemi yapay zekâ ajanınız için kopyalayın',
'ai.timedOut': '10 dakika sonra durduruldu', 'ai.timedOut': '10 dakika sonra durduruldu',
'ai.noChanges': 'Hiçbir dosya değişmedi', 'ai.noChanges': 'Hiçbir dosya değişmedi',
'ai.changed': 'Değişen dosya: {count}', 'ai.changed': 'Değişen dosya: {count}',

View file

@ -90,7 +90,9 @@ export function AiFix({ projectId, f, claude, busy, setBusy, rescan, stillThere,
); );
else if (p.kind === 'done') { else if (p.kind === 'done') {
const o = p.run.outcome; const o = p.run.outcome;
const head = o.cancelled ? t('ai.cancelled') : o.timed_out ? t('ai.timedOut') : !p.run.changes.length ? t('ai.noChanges') : t('ai.changed', { count: p.run.changes.length }); // a claude CLI that is not signed in answers at once ("Not logged in · Please run /login"): say so, offer the copy
const signedOut = !p.run.changes.length && signedOutText(o.summary);
const head = o.cancelled ? t('ai.cancelled') : o.timed_out ? t('ai.timedOut') : signedOut ? t('ai.signedOut') : !p.run.changes.length ? t('ai.noChanges') : t('ai.changed', { count: p.run.changes.length });
panel = ( panel = (
<div className="ai-panel" role="status"> <div className="ai-panel" role="status">
<span className="li-sub">{head}{p.run.changes.length > 0 && !p.undone ? ` · ${stillThere ? t('ai.stillThere') : t('ai.gone')}` : ''}</span> <span className="li-sub">{head}{p.run.changes.length > 0 && !p.undone ? ` · ${stillThere ? t('ai.stillThere') : t('ai.gone')}` : ''}</span>
@ -100,6 +102,7 @@ export function AiFix({ projectId, f, claude, busy, setBusy, rescan, stillThere,
<div className="row"> <div className="row">
{p.run.changes.length > 0 && <button className="btn btn-sm btn-primary" onClick={() => setP({ kind: 'idle' })}><Icon name="check" />{t('ai.keep')}</button>} {p.run.changes.length > 0 && <button className="btn btn-sm btn-primary" onClick={() => setP({ kind: 'idle' })}><Icon name="check" />{t('ai.keep')}</button>}
{p.run.changes.length > 0 && <button className="btn btn-sm" onClick={() => undo(p.run)}><Icon name="rotate" />{t('ai.undo')}</button>} {p.run.changes.length > 0 && <button className="btn btn-sm" onClick={() => undo(p.run)}><Icon name="rotate" />{t('ai.undo')}</button>}
{signedOut && <button className="btn btn-sm" onClick={copy}><Icon name="copy" />{t('ai.copy')}</button>}
{!p.run.changes.length && <button className="btn btn-sm" onClick={() => setP({ kind: 'idle' })}>{t('common.close')}</button>} {!p.run.changes.length && <button className="btn btn-sm" onClick={() => setP({ kind: 'idle' })}>{t('common.close')}</button>}
</div> </div>
)} )}
@ -113,3 +116,6 @@ export function AiFix({ projectId, f, claude, busy, setBusy, rescan, stillThere,
); );
return <>{p.kind === 'idle' || p.kind === 'running' ? <div className="ai-actions">{button}</div> : null}{panel}</>; return <>{p.kind === 'idle' || p.kind === 'running' ? <div className="ai-actions">{button}</div> : null}{panel}</>;
} }
/** The claude CLI's answer when it has no sign-in (it exits at once, nothing changed). */
export const signedOutText = (s: string): boolean => /not logged in|please run \/login|invalid api key|authentication/i.test(s);

View file

@ -0,0 +1,10 @@
// The claude CLI without a sign-in exits at once with this result text (seen with a real run): the AI fix panel says
// "not signed in" and offers the copy instead of a bare failure.
import { expect, test } from 'bun:test';
import { signedOutText } from '../src/ui/AiFix';
test('signed-out claude answers are recognised', () => {
expect(signedOutText('Not logged in · Please run /login')).toBe(true);
expect(signedOutText('Invalid API key · Fix external API key')).toBe(true);
expect(signedOutText('Removed NSAllowsArbitraryLoads; no http URLs are loaded.')).toBe(false);
});