Merge pull request #193 from kaitranntt/kai/fix/kiro-incognito

fix(kiro): add --no-incognito option for normal browser auth
This commit is contained in:
Kai (Tam Nhu) Tran authored and GitHub committed 2025-12-24 21:59:19 -05:00
commit b52e6d69d6
10 files changed
+133 -7

No files matched your search

+2
View File
@@ -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;
}
+16 -2
View File
@@ -126,7 +126,7 @@ export async function triggerOAuth(
options: OAuthOptions = {}
): Promise<AccountInfo | null> {
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;
}
/**
+2 -2
View File
@@ -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('');
+13
View File
@@ -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 <account> 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,
];
+1
View File
@@ -173,6 +173,7 @@ Run ${color('ccs config', 'command')} for web dashboard`.trim();
['ccs <provider> --config', 'Change model (agy, gemini)'],
['ccs <provider> --logout', 'Clear authentication'],
['ccs <provider> --headless', 'Headless auth (for SSH)'],
['ccs kiro --incognito', 'Use incognito browser (default: normal)'],
['ccs codex "explain code"', 'Use with prompt'],
]
);
+2
View File
@@ -89,6 +89,8 @@ export interface CLIProxyConfig {
variants: Record<string, CLIProxyVariantConfig>;
/** Logging configuration (disabled by default) */
logging?: CLIProxyLoggingConfig;
/** Kiro: disable incognito browser mode (use normal browser to save credentials) */
kiro_no_incognito?: boolean;
}
/**
+11 -1
View File
@@ -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<void> => {
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<voi
return;
}
// Check Kiro no-incognito setting from config (or request body)
// Default to true (use normal browser) for reliability - incognito often fails
let noIncognito = true;
if (provider === 'kiro') {
const config = loadOrCreateUnifiedConfig();
noIncognito = noIncognitoBody ?? config.cliproxy?.kiro_no_incognito ?? true;
}
try {
// Trigger OAuth flow - this opens browser and waits for completion
const account = await triggerOAuth(provider as CLIProxyProvider, {
@@ -283,6 +292,7 @@ router.post('/:provider/start', async (req: Request, res: Response): Promise<voi
headless: false, // Force interactive mode
nickname: nickname || undefined,
fromUI: true, // Enable project selection prompt in UI
noIncognito, // Kiro: use normal browser if enabled
});
if (account) {
@@ -5,7 +5,8 @@
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { User, Plus } from 'lucide-react';
import { Switch } from '@/components/ui/switch';
import { User, Plus, Globe } from 'lucide-react';
import { AccountItem } from './account-item';
import type { OAuthAccount } from '@/lib/api-client';
@@ -16,6 +17,11 @@ interface AccountsSectionProps {
onRemoveAccount: (accountId: string) => 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 (
<div>
@@ -64,6 +74,24 @@ export function AccountsSection({
<p className="text-xs opacity-70">Add an account to get started</p>
</div>
)}
{/* Kiro-specific: Incognito browser setting - users complain "it keeps opening incognito" */}
{isKiro && onKiroNoIncognitoChange && (
<div className="mt-3 pt-3 border-t border-dashed">
<div className="flex items-center justify-between gap-3">
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Globe className="w-3.5 h-3.5" />
<span>Use incognito</span>
</div>
<Switch
checked={!kiroNoIncognito}
onCheckedChange={(v) => onKiroNoIncognitoChange(!v)}
disabled={kiroSettingsLoading}
className="scale-90"
/>
</div>
</div>
)}
</div>
);
}
@@ -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"
>
<ModelConfigTab
provider={provider}
catalog={catalog}
savedPresets={savedPresets}
currentModel={currentModel}
@@ -1,16 +1,19 @@
/**
* Model Config Tab
* Contains model config section and accounts section
* Contains model config section, accounts section, and provider-specific settings
*/
import { useCallback, useEffect, useState } from 'react';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Separator } from '@/components/ui/separator';
import { ModelConfigSection } from './model-config-section';
import { AccountsSection } from './accounts-section';
import { api } from '@/lib/api-client';
import type { ProviderCatalog } from '../provider-model-selector';
import type { OAuthAccount } from '@/lib/api-client';
interface ModelConfigTabProps {
provider: string;
catalog?: ProviderCatalog;
savedPresets: Array<{
name: string;
@@ -38,6 +41,7 @@ interface ModelConfigTabProps {
}
export function ModelConfigTab({
provider,
catalog,
savedPresets,
currentModel,
@@ -57,6 +61,53 @@ export function ModelConfigTab({
isRemovingAccount,
privacyMode,
}: ModelConfigTabProps) {
// Kiro-specific: no-incognito setting (defaults to true = normal browser)
const isKiro = provider === 'kiro';
const [kiroNoIncognito, setKiroNoIncognito] = useState(true);
const [kiroSettingsLoading, setKiroSettingsLoading] = useState(true);
const [kiroSaving, setKiroSaving] = useState(false);
// Fetch Kiro settings from unified config
const fetchKiroSettings = useCallback(async () => {
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<string, unknown>;
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 (
<ScrollArea className="flex-1">
<div className="p-4 space-y-6">
@@ -82,6 +133,10 @@ export function ModelConfigTab({
onRemoveAccount={onRemoveAccount}
isRemovingAccount={isRemovingAccount}
privacyMode={privacyMode}
isKiro={isKiro}
kiroNoIncognito={kiroNoIncognito}
onKiroNoIncognitoChange={saveKiroNoIncognito}
kiroSettingsLoading={kiroSettingsLoading || kiroSaving}
/>
</div>
</ScrollArea>