setThreadError(activeThread.id, null)}
/>
{/* Main content area with optional plan sidebar */}
diff --git a/apps/web/src/components/chat/ThreadErrorBanner.tsx b/apps/web/src/components/chat/ThreadErrorBanner.tsx
index f9bc91486..9f9b54384 100644
--- a/apps/web/src/components/chat/ThreadErrorBanner.tsx
+++ b/apps/web/src/components/chat/ThreadErrorBanner.tsx
@@ -1,16 +1,21 @@
import { memo } from "react";
import { Alert, AlertAction, AlertDescription, AlertTitle } from "../ui/alert";
import { CircleAlertIcon, XIcon } from "lucide-react";
-import { humanizeThreadError } from "./threadError";
+import { humanizeThreadError, isAuthenticationThreadError } from "./threadError";
export const ThreadErrorBanner = memo(function ThreadErrorBanner({
error,
+ showAuthFailuresAsErrors = true,
onDismiss,
}: {
error: string | null;
+ showAuthFailuresAsErrors?: boolean;
onDismiss?: () => void;
}) {
if (!error) return null;
+ if (!showAuthFailuresAsErrors && isAuthenticationThreadError(error)) {
+ return null;
+ }
const presentation = humanizeThreadError(error);
return (
diff --git a/apps/web/src/components/chat/threadError.test.ts b/apps/web/src/components/chat/threadError.test.ts
index 3365cd733..bfb3e4d6d 100644
--- a/apps/web/src/components/chat/threadError.test.ts
+++ b/apps/web/src/components/chat/threadError.test.ts
@@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest";
-import { humanizeThreadError } from "./threadError";
+import { humanizeThreadError, isAuthenticationThreadError } from "./threadError";
describe("humanizeThreadError", () => {
it("summarizes worktree creation failures into a user-facing message", () => {
@@ -22,4 +22,21 @@ describe("humanizeThreadError", () => {
technicalDetails: null,
});
});
+
+ it("detects provider authentication failures", () => {
+ expect(
+ isAuthenticationThreadError(
+ "Codex CLI is not authenticated. Run `codex login` and try again.",
+ ),
+ ).toBe(true);
+ expect(
+ isAuthenticationThreadError(
+ "Claude is not authenticated. Run `claude auth login` and try again.",
+ ),
+ ).toBe(true);
+ });
+
+ it("does not classify unrelated failures as authentication errors", () => {
+ expect(isAuthenticationThreadError("Provider crashed while starting.")).toBe(false);
+ });
});
diff --git a/apps/web/src/components/chat/threadError.ts b/apps/web/src/components/chat/threadError.ts
index ac862084e..5b4ab9984 100644
--- a/apps/web/src/components/chat/threadError.ts
+++ b/apps/web/src/components/chat/threadError.ts
@@ -5,6 +5,15 @@ export interface ThreadErrorPresentation {
}
const WORKTREE_COMMAND_PREFIX = "Git command failed in GitCore.createWorktree:";
+const AUTH_FAILURE_PATTERNS = [
+ "run `codex login`",
+ "run codex login",
+ "run `claude auth login`",
+ "run claude auth login",
+ "codex cli is not authenticated",
+ "claude is not authenticated",
+ "authentication required",
+] as const;
function extractWorktreeDetail(error: string): string | null {
if (!error.startsWith(WORKTREE_COMMAND_PREFIX)) {
@@ -16,6 +25,16 @@ function extractWorktreeDetail(error: string): string | null {
return detail.length > 0 ? detail : null;
}
+export function isAuthenticationThreadError(error: string | null | undefined): boolean {
+ const trimmed = error?.trim();
+ if (!trimmed) {
+ return false;
+ }
+
+ const lower = trimmed.toLowerCase();
+ return AUTH_FAILURE_PATTERNS.some((pattern) => lower.includes(pattern));
+}
+
export function humanizeThreadError(error: string): ThreadErrorPresentation {
const trimmed = error.trim();
const worktreeDetail = extractWorktreeDetail(trimmed);
diff --git a/apps/web/src/routes/_chat.settings.tsx b/apps/web/src/routes/_chat.settings.tsx
index 2e62fd684..8a3ebd9f7 100644
--- a/apps/web/src/routes/_chat.settings.tsx
+++ b/apps/web/src/routes/_chat.settings.tsx
@@ -415,6 +415,9 @@ function SettingsRouteView() {
...(settings.enableAssistantStreaming !== defaults.enableAssistantStreaming
? ["Assistant output"]
: []),
+ ...(settings.showAuthFailuresAsErrors !== defaults.showAuthFailuresAsErrors
+ ? ["Auth failure errors"]
+ : []),
...(settings.openLinksExternally !== defaults.openLinksExternally
? ["Open links externally"]
: []),
@@ -1207,6 +1210,34 @@ function SettingsRouteView() {
}
/>
+
+ updateSettings({
+ showAuthFailuresAsErrors: defaults.showAuthFailuresAsErrors,
+ })
+ }
+ />
+ ) : null
+ }
+ control={
+
+ updateSettings({
+ showAuthFailuresAsErrors: Boolean(checked),
+ })
+ }
+ aria-label="Show authentication failures as thread errors"
+ />
+ }
+ />
+