diff --git a/src/renderer/src/lib/claude-launch-proxy-notice.test.ts b/src/renderer/src/lib/claude-launch-proxy-notice.test.ts new file mode 100644 index 00000000000..ba3e2a51e2a --- /dev/null +++ b/src/renderer/src/lib/claude-launch-proxy-notice.test.ts @@ -0,0 +1,27 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { warnIfConfiguredClaudeProxy } from './claude-launch-proxy-notice' + +const warning = vi.hoisted(() => vi.fn()) +vi.mock('sonner', () => ({ toast: { warning } })) + +describe('warnIfConfiguredClaudeProxy', () => { + beforeEach(() => warning.mockClear()) + + it('explains the configured proxy for Claude launches', () => { + warnIfConfiguredClaudeProxy('claude', { httpProxyUrl: 'http://127.0.0.1:9' }) + expect(warning).toHaveBeenCalledWith( + expect.stringContaining( + 'target host is routed through it unless covered by its bypass rules' + ), + { + duration: 12_000 + } + ) + }) + + it('does not warn for agents without a configured proxy', () => { + warnIfConfiguredClaudeProxy('codex', { httpProxyUrl: 'http://127.0.0.1:9' }) + warnIfConfiguredClaudeProxy('claude', {}) + expect(warning).not.toHaveBeenCalled() + }) +}) diff --git a/src/renderer/src/lib/claude-launch-proxy-notice.ts b/src/renderer/src/lib/claude-launch-proxy-notice.ts new file mode 100644 index 00000000000..4b09b5e3ba7 --- /dev/null +++ b/src/renderer/src/lib/claude-launch-proxy-notice.ts @@ -0,0 +1,18 @@ +import { toast } from 'sonner' +import type { TuiAgent } from '../../../shared/tui-agent' + +type ProxySettings = { httpProxyUrl?: string } + +/** Explain the app-level proxy context before a Claude process makes its first request. */ +export function warnIfConfiguredClaudeProxy( + agent: TuiAgent, + settings: ProxySettings | null | undefined +): void { + if (agent !== 'claude' || !settings?.httpProxyUrl?.trim()) { + return + } + toast.warning( + 'Orca network proxy is configured for this Claude launch; the target host is routed through it unless covered by its bypass rules. If Claude reports ConnectionRefused, check Settings → Advanced → Network.', + { duration: 12_000 } + ) +} diff --git a/src/renderer/src/lib/launch-agent-in-new-tab.test.ts b/src/renderer/src/lib/launch-agent-in-new-tab.test.ts index 5f9ce256885..29c24d2cd03 100644 --- a/src/renderer/src/lib/launch-agent-in-new-tab.test.ts +++ b/src/renderer/src/lib/launch-agent-in-new-tab.test.ts @@ -13,6 +13,7 @@ const mockSeedNativeChatLaunchDraft = vi.fn() const mockMarkNativeChatLaunchPromptFailed = vi.fn() const mockTrack = vi.fn() const mockToastMessage = vi.fn() +const mockToastWarning = vi.fn() const LEAF_ID = '11111111-1111-4111-8111-111111111111' @@ -93,9 +94,8 @@ vi.mock('@/store', () => ({ })) const mockToastError = vi.fn() - vi.mock('sonner', () => ({ - toast: { message: mockToastMessage, error: mockToastError } + toast: { message: mockToastMessage, error: mockToastError, warning: mockToastWarning } })) vi.mock('@/components/tab-bar/reconcile-order', () => ({ @@ -173,6 +173,16 @@ describe('launchAgentInNewTab', () => { mockPasteDraftWhenAgentReady.mockResolvedValue(true) }) + it('does not attribute a local proxy to Claude launches on SSH hosts', async () => { + Reflect.set(store.settings, 'httpProxyUrl', 'http://127.0.0.1:9') + store.repos = [{ id: 'repo-1', connectionId: 'ssh-target-1', path: '/repo' }] + const { launchAgentInNewTab } = await import('./launch-agent-in-new-tab') + + launchAgentInNewTab({ agent: 'claude', worktreeId: 'wt-1' }) + + expect(mockToastWarning).not.toHaveBeenCalled() + }) + it('stamps the launched agent on the new tab for immediate provider icon bootstrap', async () => { const { launchAgentInNewTab } = await import('./launch-agent-in-new-tab') diff --git a/src/renderer/src/lib/launch-agent-in-new-tab.ts b/src/renderer/src/lib/launch-agent-in-new-tab.ts index d46cb530bd6..1e2837fa796 100644 --- a/src/renderer/src/lib/launch-agent-in-new-tab.ts +++ b/src/renderer/src/lib/launch-agent-in-new-tab.ts @@ -29,6 +29,7 @@ import type { LaunchSource } from '../../../shared/telemetry-events' import { getConnectionIdFromState } from '@/lib/connection-context' import { resolveInitialNativeChatSessionOptions } from '@/components/native-chat/native-chat-launch-session-options' import { seedNativeChatAppliedSessionOptions } from '@/components/native-chat/native-chat-session-option-cache' +import { warnIfConfiguredClaudeProxy } from '@/lib/claude-launch-proxy-notice' export type LaunchAgentInNewTabArgs = { agent: TuiAgent @@ -173,6 +174,11 @@ export function launchAgentInNewTab(args: LaunchAgentInNewTabArgs): LaunchAgentI } } + // The app-level proxy is injected into local PTYs (including WSL), not SSH or paired web hosts. + if (!isRemote) { + warnIfConfiguredClaudeProxy(agent, store.settings) + } + // Why: queue startup BEFORE TerminalPane mounts — it snapshots pendingStartupByTabId in useState on first render. // Why: followup path pastes an unsubmitted draft, so gate the initial chat view like a draft launch, not auto-submit. const tab = store.createTab(worktreeId, groupId, undefined, {