Compare commits

...

7 Commits

Author SHA1 Message Date
陈大猫
98dda8a51b Merge pull request #693 from binaricat/fix/claude-acp-custom-model-provider
Some checks failed
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
fix: Claude ACP agent now uses custom API key and base URL
2026-04-12 00:51:25 +08:00
bincxz
42baa5cb78 fix: include provider base URL in ACP reuse fingerprint for Claude
The ACP provider reuse gate only computed authFingerprint for Codex,
leaving it null for Claude. Changing the configured provider or base
URL mid-session would keep reusing the stale provider instance.

Now Claude computes an authFingerprint from apiKey + baseURL, so
changing either value invalidates the cached provider and forces
recreation with the new credentials/endpoint.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 00:37:34 +08:00
bincxz
11fd7fcd71 fix: prefer anthropic provider over generic custom for Claude ACP
A generic custom provider (OpenAI-compatible) could be selected for
Claude, passing wrong credentials. Now we prefer an explicit anthropic
provider and only fall back to a custom provider when it has a baseURL
configured (indicating intentional Anthropic-compatible gateway use).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 00:31:01 +08:00
bincxz
d6950948fa fix: also inject OPENAI_BASE_URL for Codex ACP agent
Codex reads OPENAI_BASE_URL to connect to custom API endpoints.
Without this, users with a custom baseURL on their OpenAI provider
config would still hit the default api.openai.com endpoint.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 00:29:14 +08:00
bincxz
9693793bba fix: allow Claude ACP agent to use custom API key and base URL
The renderer only resolved OpenAI providers (for Codex) when passing
provider IDs to the main process. Claude agent was never matched, so
no API key was injected. Additionally, the main process only injected
CODEX_API_KEY — never ANTHROPIC_API_KEY or ANTHROPIC_BASE_URL.

Changes:
- Renderer now resolves anthropic/custom provider for Claude agent,
  openai provider for Codex agent (via matchesManagedAgentConfig)
- Main process injects ANTHROPIC_API_KEY and ANTHROPIC_BASE_URL into
  claude-agent-acp env when a provider is configured, across all three
  ACP provider creation paths (list-models, stream, fallback)

This enables users who configure an Anthropic provider with a custom
base URL (e.g. CC Switch proxy) to use Claude Code without being
redirected to the official OAuth flow.

Closes #677

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 00:26:24 +08:00
陈大猫
a72f012851 Merge pull request #692 from binaricat/fix/scrollback-zero-wheel-scroll
fix: mouse wheel scrolling broken when scrollback set to 0
2026-04-12 00:04:44 +08:00
bincxz
1368709f4e fix: map scrollback=0 to large value so mouse wheel scrolling works
xterm.js treats scrollback=0 as "no scrollback buffer", which makes
hasScrollback return false and converts wheel events into arrow-key
sequences. The UI uses 0 to mean "no limit", so map it to 999999
before passing to xterm.js.

Closes #689

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 00:00:18 +08:00
4 changed files with 57 additions and 8 deletions

View File

@@ -989,7 +989,7 @@ const TerminalComponent: React.FC<TerminalProps> = ({
if (terminalSettings) {
termRef.current.options.cursorStyle = terminalSettings.cursorShape;
termRef.current.options.cursorBlink = terminalSettings.cursorBlink;
termRef.current.options.scrollback = terminalSettings.scrollback;
termRef.current.options.scrollback = terminalSettings.scrollback === 0 ? 999999 : terminalSettings.scrollback;
termRef.current.options.fontWeight = effectiveFontWeight as
| 100
| 200

View File

@@ -30,6 +30,7 @@ import { createCattyTools } from '../../../infrastructure/ai/sdk/tools';
import type { NetcattyBridge, ExecutorContext } from '../../../infrastructure/ai/cattyAgent/executor';
import { runExternalAgentTurn } from '../../../infrastructure/ai/externalAgentAdapter';
import { runAcpAgentTurn } from '../../../infrastructure/ai/acpAgentAdapter';
import { matchesManagedAgentConfig } from '../../../infrastructure/ai/managedAgents';
import { classifyError } from '../../../infrastructure/ai/errorClassifier';
// -------------------------------------------------------------------
@@ -553,8 +554,21 @@ export function useAIChatStreaming({
// Pass only the provider ID — the main process resolves and decrypts the API key itself,
// avoiding plaintext key transit across the IPC boundary.
const openaiProvider = context.providers.find(p => p.providerId === 'openai' && p.enabled && p.apiKey);
const agentProviderId = openaiProvider?.id;
// Resolve the correct provider based on agent type:
// - Claude agent → anthropic provider (prefer over generic custom)
// - Codex agent → openai provider
const agentProviderId = (() => {
if (matchesManagedAgentConfig(agentConfig, 'claude')) {
return (
context.providers.find(p => p.providerId === 'anthropic' && p.enabled && p.apiKey)?.id
?? context.providers.find(p => p.providerId === 'custom' && p.enabled && p.apiKey && p.baseURL)?.id
);
}
if (matchesManagedAgentConfig(agentConfig, 'codex')) {
return context.providers.find(p => p.providerId === 'openai' && p.enabled && p.apiKey)?.id;
}
return undefined;
})();
// Mutable flag: set after tool-result, cleared when new assistant msg is created
let needsNewAssistantMsg = false;

View File

@@ -182,7 +182,11 @@ export const createXTermRuntime = (ctx: CreateXTermRuntimeContext): XTermRuntime
const cursorStyle = settings?.cursorShape ?? "block";
const cursorBlink = settings?.cursorBlink ?? true;
const scrollback = settings?.scrollback ?? 10000;
// xterm.js treats scrollback=0 as "no scrollback buffer", which breaks mouse
// wheel scrolling (events become arrow-key sequences). The UI uses 0 to mean
// "no limit", so map it to a large value instead.
const rawScrollback = settings?.scrollback ?? 10000;
const scrollback = rawScrollback === 0 ? 999999 : rawScrollback;
const drawBoldTextInBrightColors = settings?.drawBoldInBrightColors ?? true;
const fontWeight = resolveHostTerminalFontWeight(ctx.host, settings?.fontWeight ?? 400);
const fontWeightBold = settings?.fontWeightBold ?? 700;

View File

@@ -2103,9 +2103,18 @@ function registerHandlers(ipcMain) {
const apiKey = resolvedProvider?.apiKey || undefined;
const agentEnv = withCliDiscoveryEnv({ ...shellEnv });
if (apiKey) {
if (isCodexAgent && apiKey) {
agentEnv.CODEX_API_KEY = apiKey;
}
if (isCodexAgent && resolvedProvider?.provider?.baseURL) {
agentEnv.OPENAI_BASE_URL = resolvedProvider.provider.baseURL;
}
if (isClaudeAgent && apiKey) {
agentEnv.ANTHROPIC_API_KEY = apiKey;
}
if (isClaudeAgent && resolvedProvider?.provider?.baseURL) {
agentEnv.ANTHROPIC_BASE_URL = resolvedProvider.provider.baseURL;
}
if (isCopilotAgent) {
copilotConfigInfo = prepareCopilotHome(shellEnv, [], chatSessionId || `models_${Date.now()}`);
@@ -2266,7 +2275,11 @@ function registerHandlers(ipcMain) {
}
}
const authFingerprint = isCodexAgent ? getCodexAuthFingerprint(apiKey) : null;
const authFingerprint = isCodexAgent
? getCodexAuthFingerprint(apiKey)
: isClaudeAgent
? getCodexAuthFingerprint(apiKey + (resolvedProvider?.provider?.baseURL || ""))
: null;
const mcpSnapshot = isCodexAgent
? await resolveCodexMcpSnapshot(sessionCwd)
: { mcpServers: [], fingerprint: getCodexMcpFingerprint([]) };
@@ -2333,9 +2346,18 @@ function registerHandlers(ipcMain) {
cleanupAcpProvider(chatSessionId);
const agentEnv = withCliDiscoveryEnv({ ...shellEnv });
if (apiKey) {
if (isCodexAgent && apiKey) {
agentEnv.CODEX_API_KEY = apiKey;
}
if (isCodexAgent && resolvedProvider?.provider?.baseURL) {
agentEnv.OPENAI_BASE_URL = resolvedProvider.provider.baseURL;
}
if (isClaudeAgent && apiKey) {
agentEnv.ANTHROPIC_API_KEY = apiKey;
}
if (isClaudeAgent && resolvedProvider?.provider?.baseURL) {
agentEnv.ANTHROPIC_BASE_URL = resolvedProvider.provider.baseURL;
}
let copilotConfigInfo = null;
if (isCopilotAgent) {
copilotConfigInfo = prepareCopilotHome(shellEnv, mcpSnapshot.mcpServers, chatSessionId);
@@ -2452,8 +2474,17 @@ function registerHandlers(ipcMain) {
: acpArgs || [],
env: (() => {
const fallbackEnv = withCliDiscoveryEnv(
apiKey ? { ...shellEnv, CODEX_API_KEY: apiKey } : { ...shellEnv },
isCodexAgent && apiKey ? { ...shellEnv, CODEX_API_KEY: apiKey } : { ...shellEnv },
);
if (isCodexAgent && resolvedProvider?.provider?.baseURL) {
fallbackEnv.OPENAI_BASE_URL = resolvedProvider.provider.baseURL;
}
if (isClaudeAgent && apiKey) {
fallbackEnv.ANTHROPIC_API_KEY = apiKey;
}
if (isClaudeAgent && resolvedProvider?.provider?.baseURL) {
fallbackEnv.ANTHROPIC_BASE_URL = resolvedProvider.provider.baseURL;
}
if (isCopilotAgent) {
const fallbackCopilotConfig = prepareCopilotHome(shellEnv, mcpSnapshot.mcpServers, chatSessionId);
fallbackEnv.COPILOT_HOME = fallbackCopilotConfig.copilotHome;