Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
Original file line number Diff line number Diff line change
Expand Up @@ -2819,6 +2819,55 @@ describe("AgentChatPane submit recovery", () => {
expect(cursorCloudListAgents).not.toHaveBeenCalled();
});

it("shows the Claude login prompt when the loaded chat history contains auth notices", async () => {
const session = buildSession("session-1", {
provider: "claude",
model: "claude-sonnet-4-6",
modelId: "anthropic/claude-sonnet-4-6",
status: "idle",
});
installAdeMocks({
sessions: [session],
includeClaudeModel: true,
eventHistory: {
sessionId: session.sessionId,
sessionFound: true,
truncated: false,
events: [
{
sessionId: session.sessionId,
timestamp: "2026-06-22T12:00:00.000Z",
sequence: 1,
event: { type: "user_message", text: "hello" },
},
{
sessionId: session.sessionId,
timestamp: "2026-06-22T12:00:01.000Z",
sequence: 2,
event: {
type: "system_notice",
noticeKind: "warning",
severity: "info",
status: "authentication_failed",
message: "Claude API retry 2/10: authentication failed",
detail: "HTTP 401",
},
},
{
sessionId: session.sessionId,
timestamp: "2026-06-22T12:00:03.000Z",
sequence: 3,
event: { type: "done", turnId: "turn-1", status: "failed" },
},
],
},
});

renderPane(session);

expect(await screen.findByRole("button", { name: "Login to Claude" })).toBeTruthy();
});

it("keeps the committed model visible until the backend confirms the switch", async () => {
const session = buildSession("session-1", { status: "idle" });
const sessions = [session];
Expand Down
98 changes: 98 additions & 0 deletions apps/desktop/src/renderer/lib/claudeAuthPrompt.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,104 @@ describe("claude auth prompt helpers", () => {
})).toBe(true);
});

it("detects Claude auth failures emitted as system notices", () => {
expect(shouldShowClaudeChatLoginPrompt({
provider: "claude",
events: [
envelope({ type: "user_message", text: "hello" }),
envelope({
type: "system_notice",
noticeKind: "warning",
severity: "info",
status: "authentication_failed",
message: "Claude API retry 2/10: authentication failed",
detail: "HTTP 401",
}),
envelope({ type: "done", turnId: "turn-1", status: "failed" }),
],
})).toBe(true);
});

it("does not treat Claude auth progress notices as failures", () => {
expect(shouldShowClaudeChatLoginPrompt({
provider: "claude",
events: [
envelope({
type: "system_notice",
noticeKind: "auth",
message: "Authenticating...",
}),
],
})).toBe(false);
});

it("does not treat non-Claude auth system notices as Claude login failures", () => {
expect(shouldShowClaudeChatLoginPrompt({
provider: "claude",
events: [
envelope({
type: "system_notice",
noticeKind: "warning",
status: "authentication_failed",
message: "MCP tool failed to authenticate to GitHub",
detail: "HTTP 401",
}),
],
})).toBe(false);
});

it("does not treat generic invalid-credentials notice details as Claude login failures", () => {
expect(shouldShowClaudeChatLoginPrompt({
provider: "claude",
events: [
envelope({
type: "system_notice",
noticeKind: "hook",
message: "Hook response",
detail: "GitHub request failed: Invalid authentication credentials",
}),
],
})).toBe(false);
});

it("detects the final plaintext Claude invalid-credentials failure", () => {
expect(shouldShowClaudeChatLoginPrompt({
provider: "claude",
events: [
envelope({ type: "user_message", text: "hello" }),
envelope({
type: "text",
text: "Failed to authenticate. API Error: 401 Invalid authentication credentials",
}),
envelope({ type: "done", turnId: "turn-1", status: "failed" }),
],
})).toBe(true);
});

it("does not treat generic Claude text about auth failures as a login failure", () => {
expect(shouldShowClaudeChatLoginPrompt({
provider: "claude",
events: [
envelope({
type: "text",
text: "A GitHub request failed to authenticate because the remote server rejected it.",
}),
],
})).toBe(false);
});

it("does not treat third-party invalid-credentials text as a Claude login failure", () => {
expect(shouldShowClaudeChatLoginPrompt({
provider: "claude",
events: [
envelope({
type: "text",
text: "GitHub request failed: Invalid authentication credentials",
}),
],
})).toBe(false);
});

it("suppresses the chat prompt once Claude has produced a later successful reply", () => {
expect(shouldShowClaudeChatLoginPrompt({
provider: "claude",
Expand Down
51 changes: 43 additions & 8 deletions apps/desktop/src/renderer/lib/claudeAuthPrompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,17 @@ import type { AgentChatEvent, AgentChatEventEnvelope, TerminalSessionSummary } f

export const CLAUDE_AUTH_LOGIN_COMMAND = "claude auth login";

const CLAUDE_AUTH_ERROR_PATTERNS: RegExp[] = [
/\bplease\s+run\s+\/login\b/i,
const CLAUDE_INVALID_CREDENTIALS_PATTERNS: RegExp[] = [
/\bapi\s+error:\s*401\b.*\binvalid\s+authentication\s+credentials\b/i,
/\binvalid\s+authentication\s+credentials\b/i,
];
const CLAUDE_TEXT_AUTH_ERROR_PATTERN = /\bapi\s+error:\s*401\b.*\binvalid\s+authentication\s+credentials\b/i;

const CLAUDE_AUTH_ERROR_PATTERNS: RegExp[] = [
/\bplease\s+run\s+\/login\b/i,
...CLAUDE_INVALID_CREDENTIALS_PATTERNS,
/\bclaude\b.*\bauthentication[_\s-]*failed\b/i,
/\bclaude\b.*\bfailed\s+to\s+authenticate\b/i,
/\bclaude\b.*\b(not\s+logged\s+in|not\s+authenticated|unauthorized|authentication\s+failed|login\s+required)\b/i,
/\brun\s+[`'"]?claude\s+auth\s+login[`'"]?/i,
/\brun\s+[`'"]?claude\s+\/login[`'"]?/i,
Expand All @@ -16,9 +23,27 @@ const CLAUDE_CLI_TOOL_TYPES = new Set<TerminalSessionSummary["toolType"]>([
"claude-orchestrated",
]);

function noticeDetailText(detail: Extract<AgentChatEvent, { type: "system_notice" }>["detail"]): string {
if (typeof detail === "string") return detail;
if (!detail || typeof detail !== "object") return "";
return [
typeof detail.title === "string" ? detail.title : "",
typeof detail.summary === "string" ? detail.summary : "",
...(Array.isArray(detail.metrics) ? detail.metrics.map((metric) => `${metric.label}: ${metric.value}`) : []),
...(Array.isArray(detail.sections)
? detail.sections.flatMap((section) => [
section.title,
...section.items.map((item) => typeof item === "string" ? item : `${item.label}: ${item.value}`),
])
: []),
].filter(Boolean).join("\n");
}

function eventText(event: AgentChatEvent): string {
if (event.type !== "error") return "";
return `${event.message ?? ""}\n${event.detail ?? ""}`;
if (event.type === "error") return `${event.message ?? ""}\n${event.detail ?? ""}`;
if (event.type === "system_notice") return `${event.message ?? ""}\n${event.status ?? ""}\n${noticeDetailText(event.detail)}`;
if (event.type === "text") return event.text ?? "";
return "";
}

export function textHasClaudeAuthError(value: string | null | undefined): boolean {
Expand All @@ -28,10 +53,20 @@ export function textHasClaudeAuthError(value: string | null | undefined): boolea
}

function isClaudeAuthErrorEvent(event: AgentChatEvent): boolean {
if (event.type !== "error") return false;
const info = typeof event.errorInfo === "object" && event.errorInfo ? event.errorInfo : null;
if (info?.agentCli?.agent === "claude" && info.agentCli.category === "unauthenticated") return true;
return textHasClaudeAuthError(eventText(event));
if (event.type === "error") {
const info = typeof event.errorInfo === "object" && event.errorInfo ? event.errorInfo : null;
if (info?.agentCli?.agent === "claude" && info.agentCli.category === "unauthenticated") return true;
return textHasClaudeAuthError(eventText(event));
}
if (event.type === "system_notice") {
const text = eventText(event);
if (event.noticeKind !== "auth" && !/\bclaude\b/i.test(text)) return false;
return textHasClaudeAuthError(text);
}
if (event.type === "text") {
return CLAUDE_TEXT_AUTH_ERROR_PATTERN.test(eventText(event));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 Badge Scope text auth detection to Claude failures

When Claude has successfully produced assistant text that quotes another service's log line such as GitHub API Error: 401 Invalid authentication credentials, this branch returns true before isSuccessfulConversationBoundary can treat the text as a successful reply. That makes the chat show the Claude login prompt for unrelated credential failures or pasted/debugged error text; require a Claude-specific signal or failed-turn context for text events before surfacing the login CTA.

Useful? React with 👍 / 👎.

}
Comment thread
arul28 marked this conversation as resolved.
return false;
}

function isSuccessfulConversationBoundary(event: AgentChatEvent): boolean {
Expand Down