Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions apps/web/src/appSettings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ import {
resolveAppModelSelection,
} from "./appSettings";

describe("AppSettingsSchema", () => {
it("defaults auth failure errors to enabled", () => {
const settings = Schema.decodeUnknownSync(AppSettingsSchema)({});

expect(settings.showAuthFailuresAsErrors).toBe(true);
});
});

describe("normalizeCustomModelSlugs", () => {
it("normalizes aliases, removes built-ins, and deduplicates values", () => {
expect(
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/appSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ export const AppSettingsSchema = Schema.Struct({
autoDeleteMergedThreadsDelayMinutes: Schema.Number.pipe(withDefaults(() => 5)),
diffWordWrap: Schema.Boolean.pipe(withDefaults(() => false)),
enableAssistantStreaming: Schema.Boolean.pipe(withDefaults(() => false)),
showAuthFailuresAsErrors: Schema.Boolean.pipe(withDefaults(() => true)),
locale: AppLocale.pipe(withDefaults(() => DEFAULT_APP_LOCALE)),
openLinksExternally: Schema.Boolean.pipe(withDefaults(() => false)),
sidebarProjectSortOrder: SidebarProjectSortOrder.pipe(
Expand Down
16 changes: 13 additions & 3 deletions apps/web/src/components/BranchToolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@ import { ArrowDownIcon, FolderIcon, GitForkIcon, LoaderIcon } from "lucide-react
import { useCallback, useEffect } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";

import { gitPullMutationOptions, gitQueryKeys, gitStatusQueryOptions, invalidateGitQueries } from "../lib/gitReactQuery";
import {
gitPullMutationOptions,
gitQueryKeys,
gitStatusQueryOptions,
invalidateGitQueries,
} from "../lib/gitReactQuery";
import { newCommandId } from "../lib/utils";
import { readNativeApi } from "../nativeApi";
import { useComposerDraftStore } from "../composerDraftStore";
Expand Down Expand Up @@ -222,14 +227,19 @@ export default function BranchToolbar({
<ArrowDownIcon className="size-3" />
)}
Pull
<Badge variant="outline" size="sm" className="ml-0.5 px-1 py-0 text-[10px] text-warning border-warning/30">
<Badge
variant="outline"
size="sm"
className="ml-0.5 px-1 py-0 text-[10px] text-warning border-warning/30"
>
{behindCount}
</Badge>
</Button>
}
/>
<TooltipPopup side="bottom" align="end">
Local branch is {behindCount} commit{behindCount !== 1 ? "s" : ""} behind upstream. Pull to update before starting a new thread.
Local branch is {behindCount} commit{behindCount !== 1 ? "s" : ""} behind upstream.
Pull to update before starting a new thread.
</TooltipPopup>
</Tooltip>
) : null}
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4646,6 +4646,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
<ProviderHealthBanner status={activeProviderStatus} />
<ThreadErrorBanner
error={activeThread.error}
showAuthFailuresAsErrors={settings.showAuthFailuresAsErrors}
onDismiss={() => setThreadError(activeThread.id, null)}
/>
{/* Main content area with optional plan sidebar */}
Expand Down
7 changes: 6 additions & 1 deletion apps/web/src/components/chat/ThreadErrorBanner.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="pt-3 mx-auto max-w-7xl">
Expand Down
19 changes: 18 additions & 1 deletion apps/web/src/components/chat/threadError.test.ts
Original file line number Diff line number Diff line change
@@ -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", () => {
Expand All @@ -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);
});
});
19 changes: 19 additions & 0 deletions apps/web/src/components/chat/threadError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand All @@ -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);
Expand Down
31 changes: 31 additions & 0 deletions apps/web/src/routes/_chat.settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
: []),
Expand Down Expand Up @@ -1207,6 +1210,34 @@ function SettingsRouteView() {
}
/>

<SettingsRow
title="Auth failure errors"
description="Show provider authentication failures in the thread error banner. Turn this off to keep login issues out of the main error state."
resetAction={
settings.showAuthFailuresAsErrors !== defaults.showAuthFailuresAsErrors ? (
<SettingResetButton
label="auth failure errors"
onClick={() =>
updateSettings({
showAuthFailuresAsErrors: defaults.showAuthFailuresAsErrors,
})
}
/>
) : null
}
control={
<Switch
checked={settings.showAuthFailuresAsErrors}
onCheckedChange={(checked) =>
updateSettings({
showAuthFailuresAsErrors: Boolean(checked),
})
}
aria-label="Show authentication failures as thread errors"
/>
}
/>

<SettingsRow
title="Open links externally"
description="Open terminal URLs in your default browser instead of the embedded preview panel."
Expand Down
14 changes: 7 additions & 7 deletions docs/releases/v0.14.0/assets.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@ After the workflow completes, expect **installer and updater** artifacts similar

## Desktop installers and payloads

| Platform | Kind | Typical pattern |
| ------------------- | -------------- | ----------------- |
| macOS Apple Silicon | DMG (signed) | `*.dmg` (arm64) |
| macOS Intel | DMG (signed) | `*.dmg` (x64) |
| macOS | ZIP (updater) | `*.zip` |
| Linux x64 | AppImage | `*.AppImage` |
| Windows x64 | NSIS installer | `*.exe` |
| Platform | Kind | Typical pattern |
| ------------------- | -------------- | --------------- |
| macOS Apple Silicon | DMG (signed) | `*.dmg` (arm64) |
| macOS Intel | DMG (signed) | `*.dmg` (x64) |
| macOS | ZIP (updater) | `*.zip` |
| Linux x64 | AppImage | `*.AppImage` |
| Windows x64 | NSIS installer | `*.exe` |

### macOS code signing and notarization

Expand Down
22 changes: 19 additions & 3 deletions scripts/prepare-release.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,6 @@ After the workflow completes, expect **installer and updater** artifacts similar
| Platform | Kind | Typical pattern |
| ------------------- | -------------- | --------------- |
| macOS Apple Silicon | DMG | \`*.dmg\` (arm64) |
| macOS Intel | DMG | \`*.dmg\` (x64) |
| macOS | ZIP (updater) | \`*.zip\` |
| Linux x64 | AppImage | \`*.AppImage\` |
| Windows x64 | NSIS installer | \`*.exe\` |
Expand All @@ -297,7 +296,7 @@ After the workflow completes, expect **installer and updater** artifacts similar

| File | Purpose |
| ------------------ | --------------------------------------------------------- |
| \`latest-mac.yml\` | macOS update manifest (merged from per-arch builds in CI) |
| \`latest-mac.yml\` | macOS update manifest |
| \`latest-linux.yml\` | Linux update manifest |
| \`latest.yml\` | Windows update manifest |
| \`*.blockmap\` | Differential download block maps |
Expand Down Expand Up @@ -326,6 +325,16 @@ function updateChangelog(rootDir: string, version: string, section: string): voi
const changelogPath = resolve(rootDir, "CHANGELOG.md");
let content = readFileSync(changelogPath, "utf8");

const escapedVersion = version.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const sectionRe = new RegExp(
`\\n## \\[${escapedVersion}\\][^\\n]*\\n[\\s\\S]*?(?=\\n## \\[|\\n\\[|$)`,
"g",
);
content = content.replace(sectionRe, "\n");

const linkRe = new RegExp(`\\n\\[${escapedVersion}\\]: .*`, "g");
content = content.replace(linkRe, "");

// Insert new section after ## [Unreleased] block
const unreleasedIndex = content.indexOf("## [Unreleased]");
if (unreleasedIndex === -1) {
Expand All @@ -336,7 +345,7 @@ function updateChangelog(rootDir: string, version: string, section: string): voi
const afterUnreleased = content.indexOf("\n## [", unreleasedIndex + 1);
const insertAt = afterUnreleased !== -1 ? afterUnreleased : content.length;

content = content.slice(0, insertAt) + "\n" + section + "\n" + content.slice(insertAt);
content = content.slice(0, insertAt) + section + "\n" + content.slice(insertAt);

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

// Remove any pre-existing row for this version to keep release notes index idempotent.
const existingVersionRow = new RegExp(
`^\\| \\[${version.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\]\\(v${version.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\.md\\) \\| .*?\\|$\\n?`,
"gm",
);
content = content.replace(existingVersionRow, "");

// Find the table header separator line (| --- | --- | --- |)
const separatorRe = /^\|[ -]+\|[ -]+\|[ -]+\|$/m;
const match = content.match(separatorRe);
Expand Down
Loading