Skip to content

Commit 8f1dc4f

Browse files
committed
Hide auth failures from thread errors by default
- Add a setting to suppress provider login errors in the banner - Default the new flag on and detect Codex/Claude auth failures - Make release prep changelog updates idempotent
1 parent 0d9c5cd commit 8f1dc4f

File tree

10 files changed

+123
-15
lines changed

10 files changed

+123
-15
lines changed

apps/web/src/appSettings.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,14 @@ import {
1919
resolveAppModelSelection,
2020
} from "./appSettings";
2121

22+
describe("AppSettingsSchema", () => {
23+
it("defaults auth failure errors to enabled", () => {
24+
const settings = Schema.decodeUnknownSync(AppSettingsSchema)({});
25+
26+
expect(settings.showAuthFailuresAsErrors).toBe(true);
27+
});
28+
});
29+
2230
describe("normalizeCustomModelSlugs", () => {
2331
it("normalizes aliases, removes built-ins, and deduplicates values", () => {
2432
expect(

apps/web/src/appSettings.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ export const AppSettingsSchema = Schema.Struct({
7070
autoDeleteMergedThreadsDelayMinutes: Schema.Number.pipe(withDefaults(() => 5)),
7171
diffWordWrap: Schema.Boolean.pipe(withDefaults(() => false)),
7272
enableAssistantStreaming: Schema.Boolean.pipe(withDefaults(() => false)),
73+
showAuthFailuresAsErrors: Schema.Boolean.pipe(withDefaults(() => true)),
7374
locale: AppLocale.pipe(withDefaults(() => DEFAULT_APP_LOCALE)),
7475
openLinksExternally: Schema.Boolean.pipe(withDefaults(() => false)),
7576
sidebarProjectSortOrder: SidebarProjectSortOrder.pipe(

apps/web/src/components/BranchToolbar.tsx

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,12 @@ import { ArrowDownIcon, FolderIcon, GitForkIcon, LoaderIcon } from "lucide-react
33
import { useCallback, useEffect } from "react";
44
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
55

6-
import { gitPullMutationOptions, gitQueryKeys, gitStatusQueryOptions, invalidateGitQueries } from "../lib/gitReactQuery";
6+
import {
7+
gitPullMutationOptions,
8+
gitQueryKeys,
9+
gitStatusQueryOptions,
10+
invalidateGitQueries,
11+
} from "../lib/gitReactQuery";
712
import { newCommandId } from "../lib/utils";
813
import { readNativeApi } from "../nativeApi";
914
import { useComposerDraftStore } from "../composerDraftStore";
@@ -222,14 +227,19 @@ export default function BranchToolbar({
222227
<ArrowDownIcon className="size-3" />
223228
)}
224229
Pull
225-
<Badge variant="outline" size="sm" className="ml-0.5 px-1 py-0 text-[10px] text-warning border-warning/30">
230+
<Badge
231+
variant="outline"
232+
size="sm"
233+
className="ml-0.5 px-1 py-0 text-[10px] text-warning border-warning/30"
234+
>
226235
{behindCount}
227236
</Badge>
228237
</Button>
229238
}
230239
/>
231240
<TooltipPopup side="bottom" align="end">
232-
Local branch is {behindCount} commit{behindCount !== 1 ? "s" : ""} behind upstream. Pull to update before starting a new thread.
241+
Local branch is {behindCount} commit{behindCount !== 1 ? "s" : ""} behind upstream.
242+
Pull to update before starting a new thread.
233243
</TooltipPopup>
234244
</Tooltip>
235245
) : null}

apps/web/src/components/ChatView.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4646,6 +4646,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
46464646
<ProviderHealthBanner status={activeProviderStatus} />
46474647
<ThreadErrorBanner
46484648
error={activeThread.error}
4649+
showAuthFailuresAsErrors={settings.showAuthFailuresAsErrors}
46494650
onDismiss={() => setThreadError(activeThread.id, null)}
46504651
/>
46514652
{/* Main content area with optional plan sidebar */}

apps/web/src/components/chat/ThreadErrorBanner.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,21 @@
11
import { memo } from "react";
22
import { Alert, AlertAction, AlertDescription, AlertTitle } from "../ui/alert";
33
import { CircleAlertIcon, XIcon } from "lucide-react";
4-
import { humanizeThreadError } from "./threadError";
4+
import { humanizeThreadError, isAuthenticationThreadError } from "./threadError";
55

66
export const ThreadErrorBanner = memo(function ThreadErrorBanner({
77
error,
8+
showAuthFailuresAsErrors = true,
89
onDismiss,
910
}: {
1011
error: string | null;
12+
showAuthFailuresAsErrors?: boolean;
1113
onDismiss?: () => void;
1214
}) {
1315
if (!error) return null;
16+
if (!showAuthFailuresAsErrors && isAuthenticationThreadError(error)) {
17+
return null;
18+
}
1419
const presentation = humanizeThreadError(error);
1520
return (
1621
<div className="pt-3 mx-auto max-w-7xl">

apps/web/src/components/chat/threadError.test.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { describe, expect, it } from "vitest";
22

3-
import { humanizeThreadError } from "./threadError";
3+
import { humanizeThreadError, isAuthenticationThreadError } from "./threadError";
44

55
describe("humanizeThreadError", () => {
66
it("summarizes worktree creation failures into a user-facing message", () => {
@@ -22,4 +22,21 @@ describe("humanizeThreadError", () => {
2222
technicalDetails: null,
2323
});
2424
});
25+
26+
it("detects provider authentication failures", () => {
27+
expect(
28+
isAuthenticationThreadError(
29+
"Codex CLI is not authenticated. Run `codex login` and try again.",
30+
),
31+
).toBe(true);
32+
expect(
33+
isAuthenticationThreadError(
34+
"Claude is not authenticated. Run `claude auth login` and try again.",
35+
),
36+
).toBe(true);
37+
});
38+
39+
it("does not classify unrelated failures as authentication errors", () => {
40+
expect(isAuthenticationThreadError("Provider crashed while starting.")).toBe(false);
41+
});
2542
});

apps/web/src/components/chat/threadError.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,15 @@ export interface ThreadErrorPresentation {
55
}
66

77
const WORKTREE_COMMAND_PREFIX = "Git command failed in GitCore.createWorktree:";
8+
const AUTH_FAILURE_PATTERNS = [
9+
"run `codex login`",
10+
"run codex login",
11+
"run `claude auth login`",
12+
"run claude auth login",
13+
"codex cli is not authenticated",
14+
"claude is not authenticated",
15+
"authentication required",
16+
] as const;
817

918
function extractWorktreeDetail(error: string): string | null {
1019
if (!error.startsWith(WORKTREE_COMMAND_PREFIX)) {
@@ -16,6 +25,16 @@ function extractWorktreeDetail(error: string): string | null {
1625
return detail.length > 0 ? detail : null;
1726
}
1827

28+
export function isAuthenticationThreadError(error: string | null | undefined): boolean {
29+
const trimmed = error?.trim();
30+
if (!trimmed) {
31+
return false;
32+
}
33+
34+
const lower = trimmed.toLowerCase();
35+
return AUTH_FAILURE_PATTERNS.some((pattern) => lower.includes(pattern));
36+
}
37+
1938
export function humanizeThreadError(error: string): ThreadErrorPresentation {
2039
const trimmed = error.trim();
2140
const worktreeDetail = extractWorktreeDetail(trimmed);

apps/web/src/routes/_chat.settings.tsx

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,9 @@ function SettingsRouteView() {
415415
...(settings.enableAssistantStreaming !== defaults.enableAssistantStreaming
416416
? ["Assistant output"]
417417
: []),
418+
...(settings.showAuthFailuresAsErrors !== defaults.showAuthFailuresAsErrors
419+
? ["Auth failure errors"]
420+
: []),
418421
...(settings.openLinksExternally !== defaults.openLinksExternally
419422
? ["Open links externally"]
420423
: []),
@@ -1207,6 +1210,34 @@ function SettingsRouteView() {
12071210
}
12081211
/>
12091212

1213+
<SettingsRow
1214+
title="Auth failure errors"
1215+
description="Show provider authentication failures in the thread error banner. Turn this off to keep login issues out of the main error state."
1216+
resetAction={
1217+
settings.showAuthFailuresAsErrors !== defaults.showAuthFailuresAsErrors ? (
1218+
<SettingResetButton
1219+
label="auth failure errors"
1220+
onClick={() =>
1221+
updateSettings({
1222+
showAuthFailuresAsErrors: defaults.showAuthFailuresAsErrors,
1223+
})
1224+
}
1225+
/>
1226+
) : null
1227+
}
1228+
control={
1229+
<Switch
1230+
checked={settings.showAuthFailuresAsErrors}
1231+
onCheckedChange={(checked) =>
1232+
updateSettings({
1233+
showAuthFailuresAsErrors: Boolean(checked),
1234+
})
1235+
}
1236+
aria-label="Show authentication failures as thread errors"
1237+
/>
1238+
}
1239+
/>
1240+
12101241
<SettingsRow
12111242
title="Open links externally"
12121243
description="Open terminal URLs in your default browser instead of the embedded preview panel."

docs/releases/v0.14.0/assets.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,13 @@ After the workflow completes, expect **installer and updater** artifacts similar
1414

1515
## Desktop installers and payloads
1616

17-
| Platform | Kind | Typical pattern |
18-
| ------------------- | -------------- | ----------------- |
19-
| macOS Apple Silicon | DMG (signed) | `*.dmg` (arm64) |
20-
| macOS Intel | DMG (signed) | `*.dmg` (x64) |
21-
| macOS | ZIP (updater) | `*.zip` |
22-
| Linux x64 | AppImage | `*.AppImage` |
23-
| Windows x64 | NSIS installer | `*.exe` |
17+
| Platform | Kind | Typical pattern |
18+
| ------------------- | -------------- | --------------- |
19+
| macOS Apple Silicon | DMG (signed) | `*.dmg` (arm64) |
20+
| macOS Intel | DMG (signed) | `*.dmg` (x64) |
21+
| macOS | ZIP (updater) | `*.zip` |
22+
| Linux x64 | AppImage | `*.AppImage` |
23+
| Windows x64 | NSIS installer | `*.exe` |
2424

2525
### macOS code signing and notarization
2626

scripts/prepare-release.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,6 @@ After the workflow completes, expect **installer and updater** artifacts similar
288288
| Platform | Kind | Typical pattern |
289289
| ------------------- | -------------- | --------------- |
290290
| macOS Apple Silicon | DMG | \`*.dmg\` (arm64) |
291-
| macOS Intel | DMG | \`*.dmg\` (x64) |
292291
| macOS | ZIP (updater) | \`*.zip\` |
293292
| Linux x64 | AppImage | \`*.AppImage\` |
294293
| Windows x64 | NSIS installer | \`*.exe\` |
@@ -297,7 +296,7 @@ After the workflow completes, expect **installer and updater** artifacts similar
297296
298297
| File | Purpose |
299298
| ------------------ | --------------------------------------------------------- |
300-
| \`latest-mac.yml\` | macOS update manifest (merged from per-arch builds in CI) |
299+
| \`latest-mac.yml\` | macOS update manifest |
301300
| \`latest-linux.yml\` | Linux update manifest |
302301
| \`latest.yml\` | Windows update manifest |
303302
| \`*.blockmap\` | Differential download block maps |
@@ -326,6 +325,16 @@ function updateChangelog(rootDir: string, version: string, section: string): voi
326325
const changelogPath = resolve(rootDir, "CHANGELOG.md");
327326
let content = readFileSync(changelogPath, "utf8");
328327

328+
const escapedVersion = version.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
329+
const sectionRe = new RegExp(
330+
`\\n## \\[${escapedVersion}\\][^\\n]*\\n[\\s\\S]*?(?=\\n## \\[|\\n\\[|$)`,
331+
"g",
332+
);
333+
content = content.replace(sectionRe, "\n");
334+
335+
const linkRe = new RegExp(`\\n\\[${escapedVersion}\\]: .*`, "g");
336+
content = content.replace(linkRe, "");
337+
329338
// Insert new section after ## [Unreleased] block
330339
const unreleasedIndex = content.indexOf("## [Unreleased]");
331340
if (unreleasedIndex === -1) {
@@ -336,7 +345,7 @@ function updateChangelog(rootDir: string, version: string, section: string): voi
336345
const afterUnreleased = content.indexOf("\n## [", unreleasedIndex + 1);
337346
const insertAt = afterUnreleased !== -1 ? afterUnreleased : content.length;
338347

339-
content = content.slice(0, insertAt) + "\n" + section + "\n" + content.slice(insertAt);
348+
content = content.slice(0, insertAt) + section + "\n" + content.slice(insertAt);
340349

341350
// Add the version comparison link at the bottom
342351
const versionLink = `[${version}]: ${REPO_URL}/releases/tag/v${version}`;
@@ -356,6 +365,13 @@ function updateReleasesReadme(rootDir: string, version: string, shortDescription
356365
const readmePath = resolve(rootDir, "docs/releases/README.md");
357366
let content = readFileSync(readmePath, "utf8");
358367

368+
// Remove any pre-existing row for this version to keep release notes index idempotent.
369+
const existingVersionRow = new RegExp(
370+
`^\\| \\[${version.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\]\\(v${version.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\.md\\) \\| .*?\\|$\\n?`,
371+
"gm",
372+
);
373+
content = content.replace(existingVersionRow, "");
374+
359375
// Find the table header separator line (| --- | --- | --- |)
360376
const separatorRe = /^\|[ -]+\|[ -]+\|[ -]+\|$/m;
361377
const match = content.match(separatorRe);

0 commit comments

Comments
 (0)