mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-04-29 20:20:57 +00:00
fix settings in interactive mode
This commit is contained in:
parent
140e8c926d
commit
1e41965a7e
13 changed files with 668 additions and 9 deletions
|
|
@ -11,6 +11,7 @@ import { LoopDetectionConfirmation } from './LoopDetectionConfirmation.js';
|
|||
import { FolderTrustDialog } from './FolderTrustDialog.js';
|
||||
import { ShellConfirmationDialog } from './ShellConfirmationDialog.js';
|
||||
import { ConsentPrompt } from './ConsentPrompt.js';
|
||||
import { SettingInputPrompt } from './SettingInputPrompt.js';
|
||||
import { ThemeDialog } from './ThemeDialog.js';
|
||||
import { SettingsDialog } from './SettingsDialog.js';
|
||||
import { QwenOAuthProgress } from './QwenOAuthProgress.js';
|
||||
|
|
@ -131,6 +132,21 @@ export const DialogManager = ({
|
|||
/>
|
||||
);
|
||||
}
|
||||
if (uiState.settingInputRequests.length > 0) {
|
||||
const request = uiState.settingInputRequests[0];
|
||||
// Use settingName as key to force re-mount when switching between different settings
|
||||
return (
|
||||
<SettingInputPrompt
|
||||
key={request.settingName}
|
||||
settingName={request.settingName}
|
||||
settingDescription={request.settingDescription}
|
||||
sensitive={request.sensitive}
|
||||
onSubmit={request.onSubmit}
|
||||
onCancel={request.onCancel}
|
||||
terminalWidth={terminalWidth}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (uiState.isThemeDialogOpen) {
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
|
|
|
|||
133
packages/cli/src/ui/components/SettingInputPrompt.test.tsx
Normal file
133
packages/cli/src/ui/components/SettingInputPrompt.test.tsx
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { render } from 'ink-testing-library';
|
||||
import { SettingInputPrompt } from './SettingInputPrompt.js';
|
||||
import { TextInput } from './shared/TextInput.js';
|
||||
|
||||
vi.mock('./shared/TextInput.js', () => ({
|
||||
TextInput: vi.fn(() => null),
|
||||
}));
|
||||
|
||||
vi.mock('../hooks/useKeypress.js', () => ({
|
||||
useKeypress: vi.fn(),
|
||||
}));
|
||||
|
||||
const MockedTextInput = vi.mocked(TextInput);
|
||||
|
||||
describe('SettingInputPrompt', () => {
|
||||
const onSubmit = vi.fn();
|
||||
const onCancel = vi.fn();
|
||||
const terminalWidth = 80;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders setting name and description', () => {
|
||||
const { lastFrame } = render(
|
||||
<SettingInputPrompt
|
||||
settingName="API_KEY"
|
||||
settingDescription="Enter your API key"
|
||||
sensitive={false}
|
||||
onSubmit={onSubmit}
|
||||
onCancel={onCancel}
|
||||
terminalWidth={terminalWidth}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(lastFrame()).toContain('API_KEY');
|
||||
expect(lastFrame()).toContain('Enter your API key');
|
||||
});
|
||||
|
||||
it('renders TextInput for non-sensitive values', () => {
|
||||
render(
|
||||
<SettingInputPrompt
|
||||
settingName="USERNAME"
|
||||
settingDescription="Enter your username"
|
||||
sensitive={false}
|
||||
onSubmit={onSubmit}
|
||||
onCancel={onCancel}
|
||||
terminalWidth={terminalWidth}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(MockedTextInput).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not render TextInput for sensitive values (uses PasswordInput)', () => {
|
||||
MockedTextInput.mockClear();
|
||||
render(
|
||||
<SettingInputPrompt
|
||||
settingName="SECRET_KEY"
|
||||
settingDescription="Enter your secret key"
|
||||
sensitive={true}
|
||||
onSubmit={onSubmit}
|
||||
onCancel={onCancel}
|
||||
terminalWidth={terminalWidth}
|
||||
/>,
|
||||
);
|
||||
|
||||
// TextInput should not be called for sensitive input
|
||||
expect(MockedTextInput).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('shows masked input placeholder for sensitive mode', () => {
|
||||
const { lastFrame } = render(
|
||||
<SettingInputPrompt
|
||||
settingName="PASSWORD"
|
||||
settingDescription="Enter your password"
|
||||
sensitive={true}
|
||||
onSubmit={onSubmit}
|
||||
onCancel={onCancel}
|
||||
terminalWidth={terminalWidth}
|
||||
/>,
|
||||
);
|
||||
|
||||
// Should show the sensitive placeholder hint
|
||||
expect(lastFrame()).toContain('PASSWORD');
|
||||
expect(lastFrame()).toContain('Enter your password');
|
||||
});
|
||||
|
||||
it('displays help text for submit and cancel', () => {
|
||||
const { lastFrame } = render(
|
||||
<SettingInputPrompt
|
||||
settingName="CONFIG"
|
||||
settingDescription="Enter config value"
|
||||
sensitive={false}
|
||||
onSubmit={onSubmit}
|
||||
onCancel={onCancel}
|
||||
terminalWidth={terminalWidth}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(lastFrame()).toContain('Enter');
|
||||
expect(lastFrame()).toContain('Escape');
|
||||
});
|
||||
|
||||
it('passes correct props to TextInput for non-sensitive input', () => {
|
||||
render(
|
||||
<SettingInputPrompt
|
||||
settingName="ENDPOINT"
|
||||
settingDescription="Enter endpoint URL"
|
||||
sensitive={false}
|
||||
onSubmit={onSubmit}
|
||||
onCancel={onCancel}
|
||||
terminalWidth={terminalWidth}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(MockedTextInput).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
value: '',
|
||||
isActive: true,
|
||||
inputWidth: expect.any(Number),
|
||||
}),
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
});
|
||||
157
packages/cli/src/ui/components/SettingInputPrompt.tsx
Normal file
157
packages/cli/src/ui/components/SettingInputPrompt.tsx
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { Box, Text } from 'ink';
|
||||
import { useState } from 'react';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
import { TextInput } from './shared/TextInput.js';
|
||||
import { t } from '../../i18n/index.js';
|
||||
import { useKeypress, type Key } from '../hooks/useKeypress.js';
|
||||
import chalk from 'chalk';
|
||||
|
||||
type SettingInputPromptProps = {
|
||||
settingName: string;
|
||||
settingDescription: string;
|
||||
sensitive: boolean;
|
||||
onSubmit: (value: string) => void;
|
||||
onCancel: () => void;
|
||||
terminalWidth: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* A simple password input component that masks the input with asterisks.
|
||||
*/
|
||||
const PasswordInput = ({
|
||||
value,
|
||||
onChange,
|
||||
onSubmit,
|
||||
placeholder,
|
||||
}: {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
onSubmit: () => void;
|
||||
placeholder: string;
|
||||
}) => {
|
||||
useKeypress(
|
||||
(key: Key) => {
|
||||
// Handle submit
|
||||
if (key.name === 'return') {
|
||||
onSubmit();
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle backspace
|
||||
if (key.name === 'backspace' || key.name === 'delete') {
|
||||
onChange(value.slice(0, -1));
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle clear (Ctrl+U)
|
||||
if (key.ctrl && key.name === 'u') {
|
||||
onChange('');
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle printable characters
|
||||
if (key.sequence && !key.ctrl && !key.meta && key.sequence.length === 1) {
|
||||
const charCode = key.sequence.charCodeAt(0);
|
||||
// Only accept printable ASCII characters (32-126)
|
||||
if (charCode >= 32 && charCode <= 126) {
|
||||
onChange(value + key.sequence);
|
||||
}
|
||||
}
|
||||
},
|
||||
{ isActive: true },
|
||||
);
|
||||
|
||||
const maskedValue = '*'.repeat(value.length);
|
||||
const displayValue = maskedValue || '';
|
||||
const cursorChar = chalk.inverse(' ');
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Text color={theme.text.accent}>{'> '}</Text>
|
||||
{value.length === 0 ? (
|
||||
<Text>
|
||||
{cursorChar}
|
||||
<Text dimColor>{placeholder.slice(1)}</Text>
|
||||
</Text>
|
||||
) : (
|
||||
<Text>
|
||||
{displayValue}
|
||||
{cursorChar}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export const SettingInputPrompt = (props: SettingInputPromptProps) => {
|
||||
const {
|
||||
settingName,
|
||||
settingDescription,
|
||||
sensitive,
|
||||
onSubmit,
|
||||
onCancel,
|
||||
terminalWidth,
|
||||
} = props;
|
||||
|
||||
const [value, setValue] = useState('');
|
||||
|
||||
useKeypress(
|
||||
(key) => {
|
||||
if (key.name === 'escape') {
|
||||
onCancel();
|
||||
}
|
||||
},
|
||||
{ isActive: true },
|
||||
);
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (value.trim()) {
|
||||
onSubmit(value);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
borderStyle="round"
|
||||
borderColor={theme.border.default}
|
||||
flexDirection="column"
|
||||
paddingY={1}
|
||||
paddingX={2}
|
||||
>
|
||||
<Text bold color={theme.text.accent}>
|
||||
{settingName}
|
||||
</Text>
|
||||
<Box marginTop={1}>
|
||||
<Text>{settingDescription}</Text>
|
||||
</Box>
|
||||
<Box marginTop={1} flexDirection="column">
|
||||
{sensitive ? (
|
||||
<PasswordInput
|
||||
value={value}
|
||||
onChange={setValue}
|
||||
onSubmit={handleSubmit}
|
||||
placeholder={t('Enter sensitive value...')}
|
||||
/>
|
||||
) : (
|
||||
<TextInput
|
||||
value={value}
|
||||
onChange={setValue}
|
||||
onSubmit={handleSubmit}
|
||||
placeholder={t('Enter value...')}
|
||||
inputWidth={Math.min(terminalWidth - 10, 60)}
|
||||
isActive={true}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
<Box marginTop={1}>
|
||||
<Text dimColor>{t('Press Enter to submit, Escape to cancel')}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue