diff --git a/src/cliproxy/auth/auth-types.ts b/src/cliproxy/auth/auth-types.ts index 4bb06256..4bcc476c 100644 --- a/src/cliproxy/auth/auth-types.ts +++ b/src/cliproxy/auth/auth-types.ts @@ -171,4 +171,6 @@ export interface OAuthOptions { nickname?: string; /** If true, triggered from Web UI (enables project selection prompt) */ fromUI?: boolean; + /** If true, use --no-incognito flag (Kiro only - use normal browser instead of incognito) */ + noIncognito?: boolean; } diff --git a/src/cliproxy/auth/oauth-handler.ts b/src/cliproxy/auth/oauth-handler.ts index f2bb51c2..caba977c 100644 --- a/src/cliproxy/auth/oauth-handler.ts +++ b/src/cliproxy/auth/oauth-handler.ts @@ -126,7 +126,7 @@ export async function triggerOAuth( options: OAuthOptions = {} ): Promise { const oauthConfig = getOAuthConfig(provider); - const { verbose = false, add = false, nickname, fromUI = false } = options; + const { verbose = false, add = false, nickname, fromUI = false, noIncognito = true } = options; const callbackPort = OAUTH_PORTS[provider]; const isCLI = !fromUI; const headless = options.headless ?? isHeadlessEnvironment(); @@ -175,6 +175,10 @@ export async function triggerOAuth( if (headless) { args.push('--no-browser'); } + // Kiro-specific: --no-incognito to use normal browser (saves login credentials) + if (provider === 'kiro' && noIncognito) { + args.push('--no-incognito'); + } // Show step based on flow type if (isDeviceCodeFlow) { @@ -198,7 +202,7 @@ export async function triggerOAuth( } // Execute OAuth process - return executeOAuthProcess({ + const account = await executeOAuthProcess({ provider, binaryPath, args, @@ -210,6 +214,16 @@ export async function triggerOAuth( isCLI, nickname, }); + + // Show hint for Kiro users about --no-incognito option (first-time auth only) + if (account && provider === 'kiro' && !noIncognito) { + console.log(''); + console.log(info('Tip: To save your AWS login credentials for future sessions:')); + console.log(' Use: ccs kiro --no-incognito'); + console.log(' Or enable "Kiro: Use normal browser" in: ccs config'); + } + + return account; } /** diff --git a/src/cliproxy/auth/oauth-process.ts b/src/cliproxy/auth/oauth-process.ts index dbfa9ad4..71d58eab 100644 --- a/src/cliproxy/auth/oauth-process.ts +++ b/src/cliproxy/auth/oauth-process.ts @@ -175,8 +175,8 @@ async function handleStdout( state.browserOpened = true; } - // Display OAuth URLs in headless mode (for non-device-code flows) - if (!isDeviceCodeFlow && options.headless && !state.urlDisplayed) { + // Display OAuth URL for all modes (enables VS Code terminal URL detection popup) + if (!isDeviceCodeFlow && !state.urlDisplayed) { const urlMatch = output.match(/https?:\/\/[^\s]+/); if (urlMatch) { console.log(''); diff --git a/src/cliproxy/cliproxy-executor.ts b/src/cliproxy/cliproxy-executor.ts index 6152be27..758bf39b 100644 --- a/src/cliproxy/cliproxy-executor.ts +++ b/src/cliproxy/cliproxy-executor.ts @@ -250,6 +250,16 @@ export async function execClaudeWithCLIProxy( const forceConfig = argsWithoutProxy.includes('--config'); const addAccount = argsWithoutProxy.includes('--add'); const showAccounts = argsWithoutProxy.includes('--accounts'); + // Kiro-specific: browser mode for OAuth + // Default to normal browser (noIncognito=true) for reliability - incognito often fails on Linux + // --incognito flag opts into incognito mode, --no-incognito is legacy (now default) + const incognitoFlag = argsWithoutProxy.includes('--incognito'); + const noIncognitoFlag = argsWithoutProxy.includes('--no-incognito'); + // Config setting (defaults to true = normal browser) + const kiroNoIncognitoConfig = + provider === 'kiro' ? (unifiedConfig.cliproxy?.kiro_no_incognito ?? true) : false; + // --incognito flag overrides everything to use incognito + const noIncognito = incognitoFlag ? false : noIncognitoFlag || kiroNoIncognitoConfig; // Parse --use flag let useAccount: string | undefined; @@ -364,6 +374,7 @@ export async function execClaudeWithCLIProxy( add: addAccount, ...(forceHeadless ? { headless: true } : {}), ...(setNickname ? { nickname: setNickname } : {}), + ...(noIncognito ? { noIncognito: true } : {}), }); if (!authSuccess) { throw new Error(`Authentication required for ${providerConfig.displayName}`); @@ -591,6 +602,8 @@ export async function execClaudeWithCLIProxy( '--accounts', '--use', '--nickname', + '--incognito', + '--no-incognito', // Proxy flags are handled by resolveProxyConfig, but list for documentation ...PROXY_CLI_FLAGS, ]; diff --git a/src/commands/help-command.ts b/src/commands/help-command.ts index 224230ae..82f70a86 100644 --- a/src/commands/help-command.ts +++ b/src/commands/help-command.ts @@ -173,6 +173,7 @@ Run ${color('ccs config', 'command')} for web dashboard`.trim(); ['ccs --config', 'Change model (agy, gemini)'], ['ccs --logout', 'Clear authentication'], ['ccs --headless', 'Headless auth (for SSH)'], + ['ccs kiro --incognito', 'Use incognito browser (default: normal)'], ['ccs codex "explain code"', 'Use with prompt'], ] ); diff --git a/src/config/unified-config-types.ts b/src/config/unified-config-types.ts index 333ef307..53760348 100644 --- a/src/config/unified-config-types.ts +++ b/src/config/unified-config-types.ts @@ -89,6 +89,8 @@ export interface CLIProxyConfig { variants: Record; /** Logging configuration (disabled by default) */ logging?: CLIProxyLoggingConfig; + /** Kiro: disable incognito browser mode (use normal browser to save credentials) */ + kiro_no_incognito?: boolean; } /** diff --git a/src/web-server/routes/cliproxy-auth-routes.ts b/src/web-server/routes/cliproxy-auth-routes.ts index 1abf36d2..637cf2d3 100644 --- a/src/web-server/routes/cliproxy-auth-routes.ts +++ b/src/web-server/routes/cliproxy-auth-routes.ts @@ -23,6 +23,7 @@ import { } from '../../cliproxy/account-manager'; import { getProxyTarget } from '../../cliproxy/proxy-target-resolver'; import { fetchRemoteAuthStatus } from '../../cliproxy/remote-auth-fetcher'; +import { loadOrCreateUnifiedConfig } from '../../config/unified-config-loader'; import type { CLIProxyProvider } from '../../cliproxy/types'; const router = Router(); @@ -268,7 +269,7 @@ router.delete('/accounts/:provider/:accountId', (req: Request, res: Response): v */ router.post('/:provider/start', async (req: Request, res: Response): Promise => { const { provider } = req.params; - const { nickname } = req.body; + const { nickname, noIncognito: noIncognitoBody } = req.body; // Validate provider if (!validProviders.includes(provider as CLIProxyProvider)) { @@ -276,6 +277,14 @@ router.post('/:provider/start', async (req: Request, res: Response): Promise void; isRemovingAccount?: boolean; privacyMode?: boolean; + /** Kiro-specific: show "use normal browser" toggle */ + isKiro?: boolean; + kiroNoIncognito?: boolean; + onKiroNoIncognitoChange?: (enabled: boolean) => void; + kiroSettingsLoading?: boolean; } export function AccountsSection({ @@ -25,6 +31,10 @@ export function AccountsSection({ onRemoveAccount, isRemovingAccount, privacyMode, + isKiro, + kiroNoIncognito, + onKiroNoIncognitoChange, + kiroSettingsLoading, }: AccountsSectionProps) { return (
@@ -64,6 +74,24 @@ export function AccountsSection({

Add an account to get started

)} + + {/* Kiro-specific: Incognito browser setting - users complain "it keeps opening incognito" */} + {isKiro && onKiroNoIncognitoChange && ( +
+
+
+ + Use incognito +
+ onKiroNoIncognitoChange(!v)} + disabled={kiroSettingsLoading} + className="scale-90" + /> +
+
+ )} ); } diff --git a/ui/src/components/cliproxy/provider-editor/index.tsx b/ui/src/components/cliproxy/provider-editor/index.tsx index ab278d5e..ac17648e 100644 --- a/ui/src/components/cliproxy/provider-editor/index.tsx +++ b/ui/src/components/cliproxy/provider-editor/index.tsx @@ -157,6 +157,7 @@ export function ProviderEditor({ className="flex-1 mt-0 border-0 p-0 data-[state=inactive]:hidden flex flex-col overflow-hidden" > { + if (!isKiro) return; + try { + setKiroSettingsLoading(true); + const unifiedConfig = await api.config.get(); + const cliproxyConfig = unifiedConfig.cliproxy as { kiro_no_incognito?: boolean } | undefined; + setKiroNoIncognito(cliproxyConfig?.kiro_no_incognito ?? true); + } catch { + setKiroNoIncognito(true); + } finally { + setKiroSettingsLoading(false); + } + }, [isKiro]); + + // Save Kiro no-incognito setting + const saveKiroNoIncognito = useCallback(async (enabled: boolean) => { + setKiroNoIncognito(enabled); // Optimistic update + setKiroSaving(true); + try { + const unifiedConfig = await api.config.get(); + const existingCliproxy = (unifiedConfig.cliproxy ?? {}) as Record; + await api.config.update({ + ...unifiedConfig, + cliproxy: { + ...existingCliproxy, + kiro_no_incognito: enabled, + }, + }); + } catch { + setKiroNoIncognito(!enabled); // Revert on error + } finally { + setKiroSaving(false); + } + }, []); + + // Load Kiro settings on mount + useEffect(() => { + fetchKiroSettings(); + }, [fetchKiroSettings]); + return (
@@ -82,6 +133,10 @@ export function ModelConfigTab({ onRemoveAccount={onRemoveAccount} isRemovingAccount={isRemovingAccount} privacyMode={privacyMode} + isKiro={isKiro} + kiroNoIncognito={kiroNoIncognito} + onKiroNoIncognitoChange={saveKiroNoIncognito} + kiroSettingsLoading={kiroSettingsLoading || kiroSaving} />