mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-20 08:47:44 +00:00
Merge tag 'v0.1.21' of github.com:google-gemini/gemini-cli into chore/sync-gemini-cli-v0.1.21
This commit is contained in:
@@ -4,19 +4,20 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { Box, Text, useInput } from 'ink';
|
||||
import { Colors } from '../colors.js';
|
||||
import { RadioButtonSelect } from './shared/RadioButtonSelect.js';
|
||||
import { LoadedSettings, SettingScope } from '../../config/settings.js';
|
||||
import { AuthType } from '@qwen-code/qwen-code-core';
|
||||
import { Box, Text } from 'ink';
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
validateAuthMethod,
|
||||
setOpenAIApiKey,
|
||||
setOpenAIBaseUrl,
|
||||
setOpenAIModel,
|
||||
validateAuthMethod,
|
||||
} from '../../config/auth.js';
|
||||
import { LoadedSettings, SettingScope } from '../../config/settings.js';
|
||||
import { Colors } from '../colors.js';
|
||||
import { useKeypress } from '../hooks/useKeypress.js';
|
||||
import { OpenAIKeyPrompt } from './OpenAIKeyPrompt.js';
|
||||
import { RadioButtonSelect } from './shared/RadioButtonSelect.js';
|
||||
|
||||
interface AuthDialogProps {
|
||||
onSelect: (authMethod: AuthType | undefined, scope: SettingScope) => void;
|
||||
@@ -108,27 +109,31 @@ export function AuthDialog({
|
||||
setErrorMessage('OpenAI API key is required to use OpenAI authentication.');
|
||||
};
|
||||
|
||||
useInput((_input, key) => {
|
||||
if (showOpenAIKeyPrompt) {
|
||||
return;
|
||||
}
|
||||
useKeypress(
|
||||
(key) => {
|
||||
|
||||
if (key.escape) {
|
||||
// Prevent exit if there is an error message.
|
||||
// This means they user is not authenticated yet.
|
||||
if (errorMessage) {
|
||||
if (showOpenAIKeyPrompt) {
|
||||
return;
|
||||
}
|
||||
if (settings.merged.selectedAuthType === undefined) {
|
||||
// Prevent exiting if no auth method is set
|
||||
setErrorMessage(
|
||||
'You must select an auth method to proceed. Press Ctrl+C twice to exit.',
|
||||
);
|
||||
return;
|
||||
|
||||
if (key.name === 'escape') {
|
||||
// Prevent exit if there is an error message.
|
||||
// This means they user is not authenticated yet.
|
||||
if (errorMessage) {
|
||||
return;
|
||||
}
|
||||
if (settings.merged.selectedAuthType === undefined) {
|
||||
// Prevent exiting if no auth method is set
|
||||
setErrorMessage(
|
||||
'You must select an auth method to proceed. Press Ctrl+C twice to exit.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
onSelect(undefined, SettingScope.User);
|
||||
}
|
||||
onSelect(undefined, SettingScope.User);
|
||||
}
|
||||
});
|
||||
},
|
||||
{ isActive: true },
|
||||
);
|
||||
|
||||
if (showOpenAIKeyPrompt) {
|
||||
return (
|
||||
|
||||
@@ -5,9 +5,10 @@
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Box, Text, useInput } from 'ink';
|
||||
import { Box, Text } from 'ink';
|
||||
import Spinner from 'ink-spinner';
|
||||
import { Colors } from '../colors.js';
|
||||
import { useKeypress } from '../hooks/useKeypress.js';
|
||||
|
||||
interface AuthInProgressProps {
|
||||
onTimeout: () => void;
|
||||
@@ -18,11 +19,14 @@ export function AuthInProgress({
|
||||
}: AuthInProgressProps): React.JSX.Element {
|
||||
const [timedOut, setTimedOut] = useState(false);
|
||||
|
||||
useInput((input, key) => {
|
||||
if (key.escape || (key.ctrl && (input === 'c' || input === 'C'))) {
|
||||
onTimeout();
|
||||
}
|
||||
});
|
||||
useKeypress(
|
||||
(key) => {
|
||||
if (key.name === 'escape' || (key.ctrl && key.name === 'c')) {
|
||||
onTimeout();
|
||||
}
|
||||
},
|
||||
{ isActive: true },
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
|
||||
@@ -4,9 +4,10 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { Text, useInput } from 'ink';
|
||||
import { Text } from 'ink';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { Colors } from '../colors.js';
|
||||
import { useKeypress } from '../hooks/useKeypress.js';
|
||||
|
||||
export const DebugProfiler = () => {
|
||||
const numRenders = useRef(0);
|
||||
@@ -16,11 +17,14 @@ export const DebugProfiler = () => {
|
||||
numRenders.current++;
|
||||
});
|
||||
|
||||
useInput((input, key) => {
|
||||
if (key.ctrl && input === 'b') {
|
||||
setShowNumRenders((prev) => !prev);
|
||||
}
|
||||
});
|
||||
useKeypress(
|
||||
(key) => {
|
||||
if (key.ctrl && key.name === 'b') {
|
||||
setShowNumRenders((prev) => !prev);
|
||||
}
|
||||
},
|
||||
{ isActive: true },
|
||||
);
|
||||
|
||||
if (!showNumRenders) {
|
||||
return null;
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { Box, Text, useInput } from 'ink';
|
||||
import { Box, Text } from 'ink';
|
||||
import { Colors } from '../colors.js';
|
||||
import {
|
||||
EDITOR_DISPLAY_NAMES,
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
import { RadioButtonSelect } from './shared/RadioButtonSelect.js';
|
||||
import { LoadedSettings, SettingScope } from '../../config/settings.js';
|
||||
import { EditorType, isEditorAvailable } from '@qwen-code/qwen-code-core';
|
||||
import { useKeypress } from '../hooks/useKeypress.js';
|
||||
|
||||
interface EditorDialogProps {
|
||||
onSelect: (editorType: EditorType | undefined, scope: SettingScope) => void;
|
||||
@@ -33,14 +34,17 @@ export function EditorSettingsDialog({
|
||||
const [focusedSection, setFocusedSection] = useState<'editor' | 'scope'>(
|
||||
'editor',
|
||||
);
|
||||
useInput((_, key) => {
|
||||
if (key.tab) {
|
||||
setFocusedSection((prev) => (prev === 'editor' ? 'scope' : 'editor'));
|
||||
}
|
||||
if (key.escape) {
|
||||
onExit();
|
||||
}
|
||||
});
|
||||
useKeypress(
|
||||
(key) => {
|
||||
if (key.name === 'tab') {
|
||||
setFocusedSection((prev) => (prev === 'editor' ? 'scope' : 'editor'));
|
||||
}
|
||||
if (key.name === 'escape') {
|
||||
onExit();
|
||||
}
|
||||
},
|
||||
{ isActive: true },
|
||||
);
|
||||
|
||||
const editorItems: EditorDisplay[] =
|
||||
editorSettingsManager.getAvailableEditorDisplays();
|
||||
@@ -49,8 +53,8 @@ export function EditorSettingsDialog({
|
||||
settings.forScope(selectedScope).settings.preferredEditor;
|
||||
let editorIndex = currentPreference
|
||||
? editorItems.findIndex(
|
||||
(item: EditorDisplay) => item.type === currentPreference,
|
||||
)
|
||||
(item: EditorDisplay) => item.type === currentPreference,
|
||||
)
|
||||
: 0;
|
||||
if (editorIndex === -1) {
|
||||
console.error(`Editor is not supported: ${currentPreference}`);
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
*/
|
||||
|
||||
import { render } from 'ink-testing-library';
|
||||
import { waitFor } from '@testing-library/react';
|
||||
import { vi } from 'vitest';
|
||||
import { FolderTrustDialog, FolderTrustChoice } from './FolderTrustDialog.js';
|
||||
|
||||
@@ -18,12 +19,14 @@ describe('FolderTrustDialog', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should call onSelect with DO_NOT_TRUST when escape is pressed', () => {
|
||||
it('should call onSelect with DO_NOT_TRUST when escape is pressed', async () => {
|
||||
const onSelect = vi.fn();
|
||||
const { stdin } = render(<FolderTrustDialog onSelect={onSelect} />);
|
||||
|
||||
stdin.write('\u001B'); // Simulate escape key
|
||||
stdin.write('\x1b');
|
||||
|
||||
expect(onSelect).toHaveBeenCalledWith(FolderTrustChoice.DO_NOT_TRUST);
|
||||
await waitFor(() => {
|
||||
expect(onSelect).toHaveBeenCalledWith(FolderTrustChoice.DO_NOT_TRUST);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,13 +4,14 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { Box, Text, useInput } from 'ink';
|
||||
import { Box, Text } from 'ink';
|
||||
import React from 'react';
|
||||
import { Colors } from '../colors.js';
|
||||
import {
|
||||
RadioButtonSelect,
|
||||
RadioSelectItem,
|
||||
} from './shared/RadioButtonSelect.js';
|
||||
import { useKeypress } from '../hooks/useKeypress.js';
|
||||
|
||||
export enum FolderTrustChoice {
|
||||
TRUST_FOLDER = 'trust_folder',
|
||||
@@ -25,11 +26,14 @@ interface FolderTrustDialogProps {
|
||||
export const FolderTrustDialog: React.FC<FolderTrustDialogProps> = ({
|
||||
onSelect,
|
||||
}) => {
|
||||
useInput((_, key) => {
|
||||
if (key.escape) {
|
||||
onSelect(FolderTrustChoice.DO_NOT_TRUST);
|
||||
}
|
||||
});
|
||||
useKeypress(
|
||||
(key) => {
|
||||
if (key.name === 'escape') {
|
||||
onSelect(FolderTrustChoice.DO_NOT_TRUST);
|
||||
}
|
||||
},
|
||||
{ isActive: true },
|
||||
);
|
||||
|
||||
const options: Array<RadioSelectItem<FolderTrustChoice>> = [
|
||||
{
|
||||
|
||||
@@ -17,6 +17,7 @@ import { useShellHistory } from '../hooks/useShellHistory.js';
|
||||
import { useReverseSearchCompletion } from '../hooks/useReverseSearchCompletion.js';
|
||||
import { useCommandCompletion } from '../hooks/useCommandCompletion.js';
|
||||
import { useKeypress, Key } from '../hooks/useKeypress.js';
|
||||
import { useKittyKeyboardProtocol } from '../hooks/useKittyKeyboardProtocol.js';
|
||||
import { keyMatchers, Command } from '../keyMatchers.js';
|
||||
import { CommandContext, SlashCommand } from '../commands/types.js';
|
||||
import { Config } from '@qwen-code/qwen-code-core';
|
||||
@@ -66,6 +67,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||
const [escPressCount, setEscPressCount] = useState(0);
|
||||
const [showEscapePrompt, setShowEscapePrompt] = useState(false);
|
||||
const escapeTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const kittyProtocolStatus = useKittyKeyboardProtocol();
|
||||
|
||||
const [dirs, setDirs] = useState<readonly string[]>(
|
||||
config.getWorkspaceContext().getDirectories(),
|
||||
@@ -525,7 +527,11 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||
],
|
||||
);
|
||||
|
||||
useKeypress(handleInput, { isActive: true });
|
||||
useKeypress(handleInput, {
|
||||
isActive: true,
|
||||
kittyProtocolEnabled: kittyProtocolStatus.enabled,
|
||||
config,
|
||||
});
|
||||
|
||||
const linesToRender = buffer.viewportVisualLines;
|
||||
const [cursorVisualRowAbsolute, cursorVisualColAbsolute] =
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Box, Text, useInput } from 'ink';
|
||||
import { Box, Text } from 'ink';
|
||||
import { Colors } from '../colors.js';
|
||||
import {
|
||||
LoadedSettings,
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
getDefaultValue,
|
||||
} from '../../utils/settingsUtils.js';
|
||||
import { useVimMode } from '../contexts/VimModeContext.js';
|
||||
import { useKeypress } from '../hooks/useKeypress.js';
|
||||
|
||||
interface SettingsDialogProps {
|
||||
settings: LoadedSettings;
|
||||
@@ -256,107 +257,111 @@ export function SettingsDialog({
|
||||
const showScrollUp = true;
|
||||
const showScrollDown = true;
|
||||
|
||||
useInput((input, key) => {
|
||||
if (key.tab) {
|
||||
setFocusSection((prev) => (prev === 'settings' ? 'scope' : 'settings'));
|
||||
}
|
||||
if (focusSection === 'settings') {
|
||||
if (key.upArrow || input === 'k') {
|
||||
const newIndex =
|
||||
activeSettingIndex > 0 ? activeSettingIndex - 1 : items.length - 1;
|
||||
setActiveSettingIndex(newIndex);
|
||||
// Adjust scroll offset for wrap-around
|
||||
if (newIndex === items.length - 1) {
|
||||
setScrollOffset(Math.max(0, items.length - maxItemsToShow));
|
||||
} else if (newIndex < scrollOffset) {
|
||||
setScrollOffset(newIndex);
|
||||
}
|
||||
} else if (key.downArrow || input === 'j') {
|
||||
const newIndex =
|
||||
activeSettingIndex < items.length - 1 ? activeSettingIndex + 1 : 0;
|
||||
setActiveSettingIndex(newIndex);
|
||||
// Adjust scroll offset for wrap-around
|
||||
if (newIndex === 0) {
|
||||
setScrollOffset(0);
|
||||
} else if (newIndex >= scrollOffset + maxItemsToShow) {
|
||||
setScrollOffset(newIndex - maxItemsToShow + 1);
|
||||
}
|
||||
} else if (key.return || input === ' ') {
|
||||
items[activeSettingIndex]?.toggle();
|
||||
} else if ((key.ctrl && input === 'c') || (key.ctrl && input === 'l')) {
|
||||
// Ctrl+C or Ctrl+L: Clear current setting and reset to default
|
||||
const currentSetting = items[activeSettingIndex];
|
||||
if (currentSetting) {
|
||||
const defaultValue = getDefaultValue(currentSetting.value);
|
||||
// Ensure defaultValue is a boolean for setPendingSettingValue
|
||||
const booleanDefaultValue =
|
||||
typeof defaultValue === 'boolean' ? defaultValue : false;
|
||||
useKeypress(
|
||||
(key) => {
|
||||
const { name, ctrl } = key;
|
||||
if (name === 'tab') {
|
||||
setFocusSection((prev) => (prev === 'settings' ? 'scope' : 'settings'));
|
||||
}
|
||||
if (focusSection === 'settings') {
|
||||
if (name === 'up' || name === 'k') {
|
||||
const newIndex =
|
||||
activeSettingIndex > 0 ? activeSettingIndex - 1 : items.length - 1;
|
||||
setActiveSettingIndex(newIndex);
|
||||
// Adjust scroll offset for wrap-around
|
||||
if (newIndex === items.length - 1) {
|
||||
setScrollOffset(Math.max(0, items.length - maxItemsToShow));
|
||||
} else if (newIndex < scrollOffset) {
|
||||
setScrollOffset(newIndex);
|
||||
}
|
||||
} else if (name === 'down' || name === 'j') {
|
||||
const newIndex =
|
||||
activeSettingIndex < items.length - 1 ? activeSettingIndex + 1 : 0;
|
||||
setActiveSettingIndex(newIndex);
|
||||
// Adjust scroll offset for wrap-around
|
||||
if (newIndex === 0) {
|
||||
setScrollOffset(0);
|
||||
} else if (newIndex >= scrollOffset + maxItemsToShow) {
|
||||
setScrollOffset(newIndex - maxItemsToShow + 1);
|
||||
}
|
||||
} else if (name === 'return' || name === 'space') {
|
||||
items[activeSettingIndex]?.toggle();
|
||||
} else if (ctrl && (name === 'c' || name === 'l')) {
|
||||
// Ctrl+C or Ctrl+L: Clear current setting and reset to default
|
||||
const currentSetting = items[activeSettingIndex];
|
||||
if (currentSetting) {
|
||||
const defaultValue = getDefaultValue(currentSetting.value);
|
||||
// Ensure defaultValue is a boolean for setPendingSettingValue
|
||||
const booleanDefaultValue =
|
||||
typeof defaultValue === 'boolean' ? defaultValue : false;
|
||||
|
||||
// Update pending settings to default value
|
||||
setPendingSettings((prev) =>
|
||||
setPendingSettingValue(
|
||||
currentSetting.value,
|
||||
booleanDefaultValue,
|
||||
prev,
|
||||
),
|
||||
);
|
||||
|
||||
// Remove from modified settings since it's now at default
|
||||
setModifiedSettings((prev) => {
|
||||
const updated = new Set(prev);
|
||||
updated.delete(currentSetting.value);
|
||||
return updated;
|
||||
});
|
||||
|
||||
// Remove from restart-required settings if it was there
|
||||
setRestartRequiredSettings((prev) => {
|
||||
const updated = new Set(prev);
|
||||
updated.delete(currentSetting.value);
|
||||
return updated;
|
||||
});
|
||||
|
||||
// If this setting doesn't require restart, save it immediately
|
||||
if (!requiresRestart(currentSetting.value)) {
|
||||
const immediateSettings = new Set([currentSetting.value]);
|
||||
const immediateSettingsObject = setPendingSettingValue(
|
||||
currentSetting.value,
|
||||
booleanDefaultValue,
|
||||
{},
|
||||
// Update pending settings to default value
|
||||
setPendingSettings((prev) =>
|
||||
setPendingSettingValue(
|
||||
currentSetting.value,
|
||||
booleanDefaultValue,
|
||||
prev,
|
||||
),
|
||||
);
|
||||
|
||||
saveModifiedSettings(
|
||||
immediateSettings,
|
||||
immediateSettingsObject,
|
||||
settings,
|
||||
selectedScope,
|
||||
);
|
||||
// Remove from modified settings since it's now at default
|
||||
setModifiedSettings((prev) => {
|
||||
const updated = new Set(prev);
|
||||
updated.delete(currentSetting.value);
|
||||
return updated;
|
||||
});
|
||||
|
||||
// Remove from restart-required settings if it was there
|
||||
setRestartRequiredSettings((prev) => {
|
||||
const updated = new Set(prev);
|
||||
updated.delete(currentSetting.value);
|
||||
return updated;
|
||||
});
|
||||
|
||||
// If this setting doesn't require restart, save it immediately
|
||||
if (!requiresRestart(currentSetting.value)) {
|
||||
const immediateSettings = new Set([currentSetting.value]);
|
||||
const immediateSettingsObject = setPendingSettingValue(
|
||||
currentSetting.value,
|
||||
booleanDefaultValue,
|
||||
{},
|
||||
);
|
||||
|
||||
saveModifiedSettings(
|
||||
immediateSettings,
|
||||
immediateSettingsObject,
|
||||
settings,
|
||||
selectedScope,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (showRestartPrompt && input === 'r') {
|
||||
// Only save settings that require restart (non-restart settings were already saved immediately)
|
||||
const restartRequiredSettings =
|
||||
getRestartRequiredFromModified(modifiedSettings);
|
||||
const restartRequiredSet = new Set(restartRequiredSettings);
|
||||
if (showRestartPrompt && name === 'r') {
|
||||
// Only save settings that require restart (non-restart settings were already saved immediately)
|
||||
const restartRequiredSettings =
|
||||
getRestartRequiredFromModified(modifiedSettings);
|
||||
const restartRequiredSet = new Set(restartRequiredSettings);
|
||||
|
||||
if (restartRequiredSet.size > 0) {
|
||||
saveModifiedSettings(
|
||||
restartRequiredSet,
|
||||
pendingSettings,
|
||||
settings,
|
||||
selectedScope,
|
||||
);
|
||||
if (restartRequiredSet.size > 0) {
|
||||
saveModifiedSettings(
|
||||
restartRequiredSet,
|
||||
pendingSettings,
|
||||
settings,
|
||||
selectedScope,
|
||||
);
|
||||
}
|
||||
|
||||
setShowRestartPrompt(false);
|
||||
setRestartRequiredSettings(new Set()); // Clear restart-required settings
|
||||
if (onRestartRequest) onRestartRequest();
|
||||
}
|
||||
|
||||
setShowRestartPrompt(false);
|
||||
setRestartRequiredSettings(new Set()); // Clear restart-required settings
|
||||
if (onRestartRequest) onRestartRequest();
|
||||
}
|
||||
if (key.escape) {
|
||||
onSelect(undefined, selectedScope);
|
||||
}
|
||||
});
|
||||
if (name === 'escape') {
|
||||
onSelect(undefined, selectedScope);
|
||||
}
|
||||
},
|
||||
{ isActive: true },
|
||||
);
|
||||
|
||||
return (
|
||||
<Box
|
||||
|
||||
@@ -5,9 +5,10 @@
|
||||
*/
|
||||
|
||||
import { ToolConfirmationOutcome } from '@qwen-code/qwen-code-core';
|
||||
import { Box, Text, useInput } from 'ink';
|
||||
import { Box, Text } from 'ink';
|
||||
import React from 'react';
|
||||
import { Colors } from '../colors.js';
|
||||
import { useKeypress } from '../hooks/useKeypress.js';
|
||||
import {
|
||||
RadioButtonSelect,
|
||||
RadioSelectItem,
|
||||
@@ -30,11 +31,14 @@ export const ShellConfirmationDialog: React.FC<
|
||||
> = ({ request }) => {
|
||||
const { commands, onConfirm } = request;
|
||||
|
||||
useInput((_, key) => {
|
||||
if (key.escape) {
|
||||
onConfirm(ToolConfirmationOutcome.Cancel);
|
||||
}
|
||||
});
|
||||
useKeypress(
|
||||
(key) => {
|
||||
if (key.name === 'escape') {
|
||||
onConfirm(ToolConfirmationOutcome.Cancel);
|
||||
}
|
||||
},
|
||||
{ isActive: true },
|
||||
);
|
||||
|
||||
const handleSelect = (item: ToolConfirmationOutcome) => {
|
||||
if (item === ToolConfirmationOutcome.Cancel) {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { Box, Text, useInput } from 'ink';
|
||||
import { Box, Text } from 'ink';
|
||||
import { Colors } from '../colors.js';
|
||||
import { themeManager, DEFAULT_THEME } from '../themes/theme-manager.js';
|
||||
import { RadioButtonSelect } from './shared/RadioButtonSelect.js';
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
getScopeItems,
|
||||
getScopeMessageForSetting,
|
||||
} from '../../utils/dialogScopeUtils.js';
|
||||
import { useKeypress } from '../hooks/useKeypress.js';
|
||||
|
||||
interface ThemeDialogProps {
|
||||
/** Callback function when a theme is selected */
|
||||
@@ -111,14 +112,17 @@ export function ThemeDialog({
|
||||
'theme',
|
||||
);
|
||||
|
||||
useInput((input, key) => {
|
||||
if (key.tab) {
|
||||
setFocusedSection((prev) => (prev === 'theme' ? 'scope' : 'theme'));
|
||||
}
|
||||
if (key.escape) {
|
||||
onSelect(undefined, selectedScope);
|
||||
}
|
||||
});
|
||||
useKeypress(
|
||||
(key) => {
|
||||
if (key.name === 'tab') {
|
||||
setFocusedSection((prev) => (prev === 'theme' ? 'scope' : 'theme'));
|
||||
}
|
||||
if (key.name === 'escape') {
|
||||
onSelect(undefined, selectedScope);
|
||||
}
|
||||
},
|
||||
{ isActive: true },
|
||||
);
|
||||
|
||||
// Generate scope message for theme setting
|
||||
const otherScopeModifiedMessage = getScopeMessageForSetting(
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Box, Text, useInput } from 'ink';
|
||||
import { Box, Text } from 'ink';
|
||||
import { DiffRenderer } from './DiffRenderer.js';
|
||||
import { Colors } from '../../colors.js';
|
||||
import {
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
RadioSelectItem,
|
||||
} from '../shared/RadioButtonSelect.js';
|
||||
import { MaxSizedBox } from '../shared/MaxSizedBox.js';
|
||||
import { useKeypress } from '../../hooks/useKeypress.js';
|
||||
|
||||
export interface ToolConfirmationMessageProps {
|
||||
confirmationDetails: ToolCallConfirmationDetails;
|
||||
@@ -44,7 +45,7 @@ export const ToolConfirmationMessage: React.FC<
|
||||
const handleConfirm = async (outcome: ToolConfirmationOutcome) => {
|
||||
if (confirmationDetails.type === 'edit') {
|
||||
const ideClient = config?.getIdeClient();
|
||||
if (config?.getIdeMode() && config?.getIdeModeFeature()) {
|
||||
if (config?.getIdeMode()) {
|
||||
const cliOutcome =
|
||||
outcome === ToolConfirmationOutcome.Cancel ? 'rejected' : 'accepted';
|
||||
await ideClient?.resolveDiffFromCli(
|
||||
@@ -56,12 +57,15 @@ export const ToolConfirmationMessage: React.FC<
|
||||
onConfirm(outcome);
|
||||
};
|
||||
|
||||
useInput((input, key) => {
|
||||
if (!isFocused) return;
|
||||
if (key.escape || (key.ctrl && (input === 'c' || input === 'C'))) {
|
||||
handleConfirm(ToolConfirmationOutcome.Cancel);
|
||||
}
|
||||
});
|
||||
useKeypress(
|
||||
(key) => {
|
||||
if (!isFocused) return;
|
||||
if (key.name === 'escape' || (key.ctrl && key.name === 'c')) {
|
||||
handleConfirm(ToolConfirmationOutcome.Cancel);
|
||||
}
|
||||
},
|
||||
{ isActive: isFocused },
|
||||
);
|
||||
|
||||
const handleSelect = (item: ToolConfirmationOutcome) => handleConfirm(item);
|
||||
|
||||
@@ -132,7 +136,7 @@ export const ToolConfirmationMessage: React.FC<
|
||||
value: ToolConfirmationOutcome.ProceedAlways,
|
||||
},
|
||||
);
|
||||
if (config?.getIdeMode() && config?.getIdeModeFeature()) {
|
||||
if (config?.getIdeMode()) {
|
||||
options.push({
|
||||
label: 'No (esc)',
|
||||
value: ToolConfirmationOutcome.Cancel,
|
||||
|
||||
@@ -5,8 +5,9 @@
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import { Text, Box, useInput } from 'ink';
|
||||
import { Text, Box } from 'ink';
|
||||
import { Colors } from '../../colors.js';
|
||||
import { useKeypress } from '../../hooks/useKeypress.js';
|
||||
|
||||
/**
|
||||
* Represents a single option for the RadioButtonSelect.
|
||||
@@ -85,9 +86,10 @@ export function RadioButtonSelect<T>({
|
||||
[],
|
||||
);
|
||||
|
||||
useInput(
|
||||
(input, key) => {
|
||||
const isNumeric = showNumbers && /^[0-9]$/.test(input);
|
||||
useKeypress(
|
||||
(key) => {
|
||||
const { sequence, name } = key;
|
||||
const isNumeric = showNumbers && /^[0-9]$/.test(sequence);
|
||||
|
||||
// Any key press that is not a digit should clear the number input buffer.
|
||||
if (!isNumeric && numberInputTimer.current) {
|
||||
@@ -95,21 +97,21 @@ export function RadioButtonSelect<T>({
|
||||
setNumberInput('');
|
||||
}
|
||||
|
||||
if (input === 'k' || key.upArrow) {
|
||||
if (name === 'k' || name === 'up') {
|
||||
const newIndex = activeIndex > 0 ? activeIndex - 1 : items.length - 1;
|
||||
setActiveIndex(newIndex);
|
||||
onHighlight?.(items[newIndex]!.value);
|
||||
return;
|
||||
}
|
||||
|
||||
if (input === 'j' || key.downArrow) {
|
||||
if (name === 'j' || name === 'down') {
|
||||
const newIndex = activeIndex < items.length - 1 ? activeIndex + 1 : 0;
|
||||
setActiveIndex(newIndex);
|
||||
onHighlight?.(items[newIndex]!.value);
|
||||
return;
|
||||
}
|
||||
|
||||
if (key.return) {
|
||||
if (name === 'return') {
|
||||
onSelect(items[activeIndex]!.value);
|
||||
return;
|
||||
}
|
||||
@@ -120,7 +122,7 @@ export function RadioButtonSelect<T>({
|
||||
clearTimeout(numberInputTimer.current);
|
||||
}
|
||||
|
||||
const newNumberInput = numberInput + input;
|
||||
const newNumberInput = numberInput + sequence;
|
||||
setNumberInput(newNumberInput);
|
||||
|
||||
const targetIndex = Number.parseInt(newNumberInput, 10) - 1;
|
||||
@@ -154,7 +156,7 @@ export function RadioButtonSelect<T>({
|
||||
}
|
||||
}
|
||||
},
|
||||
{ isActive: isFocused && items.length > 0 },
|
||||
{ isActive: !!(isFocused && items.length > 0) },
|
||||
);
|
||||
|
||||
const visibleItems = items.slice(scrollOffset, scrollOffset + maxItemsToShow);
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import stripAnsi from 'strip-ansi';
|
||||
import { renderHook, act } from '@testing-library/react';
|
||||
import {
|
||||
useTextBuffer,
|
||||
@@ -1278,6 +1279,45 @@ Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots
|
||||
);
|
||||
expect(getBufferState(result).text).toBe('Pasted Text');
|
||||
});
|
||||
|
||||
it('should not strip popular emojis', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useTextBuffer({ viewport, isValidPath: () => false }),
|
||||
);
|
||||
const emojis = '🐍🐳🦀🦄';
|
||||
act(() =>
|
||||
result.current.handleInput({
|
||||
name: '',
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: false,
|
||||
paste: false,
|
||||
sequence: emojis,
|
||||
}),
|
||||
);
|
||||
expect(getBufferState(result).text).toBe(emojis);
|
||||
});
|
||||
});
|
||||
|
||||
describe('stripAnsi', () => {
|
||||
it('should correctly strip ANSI escape codes', () => {
|
||||
const textWithAnsi = '\x1B[31mHello\x1B[0m World';
|
||||
expect(stripAnsi(textWithAnsi)).toBe('Hello World');
|
||||
});
|
||||
|
||||
it('should handle multiple ANSI codes', () => {
|
||||
const textWithMultipleAnsi = '\x1B[1m\x1B[34mBold Blue\x1B[0m Text';
|
||||
expect(stripAnsi(textWithMultipleAnsi)).toBe('Bold Blue Text');
|
||||
});
|
||||
|
||||
it('should not modify text without ANSI codes', () => {
|
||||
const plainText = 'Plain text';
|
||||
expect(stripAnsi(plainText)).toBe('Plain text');
|
||||
});
|
||||
|
||||
it('should handle empty string', () => {
|
||||
expect(stripAnsi('')).toBe('');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
*/
|
||||
|
||||
import stripAnsi from 'strip-ansi';
|
||||
import { stripVTControlCharacters } from 'util';
|
||||
import { spawnSync } from 'child_process';
|
||||
import fs from 'fs';
|
||||
import os from 'os';
|
||||
@@ -496,21 +497,44 @@ export const replaceRangeInternal = (
|
||||
/**
|
||||
* Strip characters that can break terminal rendering.
|
||||
*
|
||||
* Strip ANSI escape codes and control characters except for line breaks.
|
||||
* Control characters such as delete break terminal UI rendering.
|
||||
* Uses Node.js built-in stripVTControlCharacters to handle VT sequences,
|
||||
* then filters remaining control characters that can disrupt display.
|
||||
*
|
||||
* Characters stripped:
|
||||
* - ANSI escape sequences (via strip-ansi)
|
||||
* - VT control sequences (via Node.js util.stripVTControlCharacters)
|
||||
* - C0 control chars (0x00-0x1F) except CR/LF which are handled elsewhere
|
||||
* - C1 control chars (0x80-0x9F) that can cause display issues
|
||||
*
|
||||
* Characters preserved:
|
||||
* - All printable Unicode including emojis
|
||||
* - DEL (0x7F) - handled functionally by applyOperations, not a display issue
|
||||
* - CR/LF (0x0D/0x0A) - needed for line breaks
|
||||
*/
|
||||
function stripUnsafeCharacters(str: string): string {
|
||||
const stripped = stripAnsi(str);
|
||||
return toCodePoints(stripped)
|
||||
const strippedAnsi = stripAnsi(str);
|
||||
const strippedVT = stripVTControlCharacters(strippedAnsi);
|
||||
|
||||
return toCodePoints(strippedVT)
|
||||
.filter((char) => {
|
||||
if (char.length > 1) return false;
|
||||
const code = char.codePointAt(0);
|
||||
if (code === undefined) {
|
||||
return false;
|
||||
}
|
||||
const isUnsafe =
|
||||
code === 127 || (code <= 31 && code !== 13 && code !== 10);
|
||||
return !isUnsafe;
|
||||
if (code === undefined) return false;
|
||||
|
||||
// Preserve CR/LF for line handling
|
||||
if (code === 0x0a || code === 0x0d) return true;
|
||||
|
||||
// Remove C0 control chars (except CR/LF) that can break display
|
||||
// Examples: BELL(0x07) makes noise, BS(0x08) moves cursor, VT(0x0B), FF(0x0C)
|
||||
if (code >= 0x00 && code <= 0x1f) return false;
|
||||
|
||||
// Remove C1 control chars (0x80-0x9F) - legacy 8-bit control codes
|
||||
if (code >= 0x80 && code <= 0x9f) return false;
|
||||
|
||||
// Preserve DEL (0x7F) - it's handled functionally by applyOperations as backspace
|
||||
// and doesn't cause rendering issues when displayed
|
||||
|
||||
// Preserve all other characters including Unicode/emojis
|
||||
return true;
|
||||
})
|
||||
.join('');
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
findWordEndInLine,
|
||||
} from './text-buffer.js';
|
||||
import { cpLen, toCodePoints } from '../../utils/textUtils.js';
|
||||
import { assumeExhaustive } from '../../../utils/checks.js';
|
||||
|
||||
// Check if we're at the end of a base word (on the last base character)
|
||||
// Returns true if current position has a base character followed only by combining marks until non-word
|
||||
@@ -806,7 +807,7 @@ export function handleVimAction(
|
||||
|
||||
default: {
|
||||
// This should never happen if TypeScript is working correctly
|
||||
const _exhaustiveCheck: never = action;
|
||||
assumeExhaustive(action);
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user