feat: Refactor feedback dialog to a non-blocking popup, allow user input while it is rendered

This commit is contained in:
DragonnZhang 2026-01-14 11:48:03 +08:00
parent 9325721811
commit d91e372c72
6 changed files with 48 additions and 24 deletions

View file

@ -1208,8 +1208,7 @@ export const AppContainer = (props: AppContainerProps) => {
isSubagentCreateDialogOpen ||
isAgentsManagerDialogOpen ||
isApprovalModeDialogOpen ||
isResumeDialogOpen ||
isFeedbackDialogOpen;
isResumeDialogOpen;
const pendingHistoryItems = useMemo(
() => [...pendingSlashCommandHistoryItems, ...pendingGeminiHistoryItems],

View file

@ -5,31 +5,39 @@ import { useUIActions } from './contexts/UIActionsContext.js';
import { useUIState } from './contexts/UIStateContext.js';
import { useKeypress } from './hooks/useKeypress.js';
const FEEDBACK_OPTIONS = {
GOOD: 1,
BAD: 2,
NOT_SURE: 3,
} as const;
const FEEDBACK_OPTION_KEYS = {
[FEEDBACK_OPTIONS.GOOD]: '1',
[FEEDBACK_OPTIONS.BAD]: '2',
[FEEDBACK_OPTIONS.NOT_SURE]: '3',
} as const;
export const FEEDBACK_DIALOG_KEYS = ['1', '2', '3'] as const;
export const FeedbackDialog: React.FC = () => {
const uiState = useUIState();
const uiActions = useUIActions();
useKeypress(
(key) => {
if (key.name === 'escape') {
uiActions.closeFeedbackDialog();
} else if (key.name === '1') {
uiActions.submitFeedback(1);
} else if (key.name === '2') {
uiActions.submitFeedback(2);
} else if (key.name === '3') {
uiActions.submitFeedback(3);
} else if (key.name === '0') {
uiActions.closeFeedbackDialog();
if (key.name === FEEDBACK_OPTION_KEYS[FEEDBACK_OPTIONS.GOOD]) {
uiActions.submitFeedback(FEEDBACK_OPTIONS.GOOD);
} else if (key.name === FEEDBACK_OPTION_KEYS[FEEDBACK_OPTIONS.BAD]) {
uiActions.submitFeedback(FEEDBACK_OPTIONS.BAD);
} else if (key.name === FEEDBACK_OPTION_KEYS[FEEDBACK_OPTIONS.NOT_SURE]) {
uiActions.submitFeedback(FEEDBACK_OPTIONS.NOT_SURE);
}
uiActions.closeFeedbackDialog();
},
{ isActive: uiState.isFeedbackDialogOpen },
);
if (!uiState.isFeedbackDialogOpen) {
return null;
}
return (
<Box flexDirection="column" marginY={1}>
<Box>
@ -37,13 +45,17 @@ export const FeedbackDialog: React.FC = () => {
<Text bold>{t('How is Qwen doing this session? (optional)')}</Text>
</Box>
<Box marginTop={1}>
<Text color="cyan">1: </Text>
<Text color="cyan">
{FEEDBACK_OPTION_KEYS[FEEDBACK_OPTIONS.GOOD]}:{' '}
</Text>
<Text>{t('Good')}</Text>
<Text> </Text>
<Text color="cyan">2: </Text>
<Text color="cyan">{FEEDBACK_OPTION_KEYS[FEEDBACK_OPTIONS.BAD]}: </Text>
<Text>{t('Bad')}</Text>
<Text> </Text>
<Text color="cyan">3: </Text>
<Text color="cyan">
{FEEDBACK_OPTION_KEYS[FEEDBACK_OPTIONS.NOT_SURE]}:{' '}
</Text>
<Text>{t('Not Sure Yet')}</Text>
</Box>
</Box>

View file

@ -26,6 +26,7 @@ import { useSettings } from '../contexts/SettingsContext.js';
import { ApprovalMode } from '@qwen-code/qwen-code-core';
import { StreamingState } from '../types.js';
import { ConfigInitDisplay } from '../components/ConfigInitDisplay.js';
import { FeedbackDialog } from '../FeedbackDialog.js';
import { t } from '../../i18n/index.js';
export const Composer = () => {
@ -134,6 +135,8 @@ export const Composer = () => {
</OverflowProvider>
)}
{uiState.isFeedbackDialogOpen && <FeedbackDialog />}
{uiState.isInputActive && (
<InputPrompt
buffer={uiState.buffer}

View file

@ -35,7 +35,6 @@ import { ModelSwitchDialog } from './ModelSwitchDialog.js';
import { AgentCreationWizard } from './subagents/create/AgentCreationWizard.js';
import { AgentsManagerDialog } from './subagents/manage/AgentsManagerDialog.js';
import { SessionPicker } from './SessionPicker.js';
import { FeedbackDialog } from '../FeedbackDialog.js';
interface DialogManagerProps {
addItem: UseHistoryManagerReturn['addItem'];
@ -292,9 +291,5 @@ export const DialogManager = ({
);
}
if (uiState.isFeedbackDialogOpen) {
return <FeedbackDialog />;
}
return null;
};

View file

@ -33,6 +33,9 @@ vi.mock('../hooks/useCommandCompletion.js');
vi.mock('../hooks/useInputHistory.js');
vi.mock('../hooks/useReverseSearchCompletion.js');
vi.mock('../utils/clipboardUtils.js');
vi.mock('../contexts/UIStateContext.js', () => ({
useUIState: vi.fn(() => ({ isFeedbackDialogOpen: false })),
}));
const mockSlashCommands: SlashCommand[] = [
{

View file

@ -36,6 +36,8 @@ import {
import * as path from 'node:path';
import { SCREEN_READER_USER_PREFIX } from '../textConstants.js';
import { useShellFocusState } from '../contexts/ShellFocusContext.js';
import { useUIState } from '../contexts/UIStateContext.js';
import { FEEDBACK_DIALOG_KEYS } from '../FeedbackDialog.js';
export interface InputPromptProps {
buffer: TextBuffer;
onSubmit: (value: string) => void;
@ -100,6 +102,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
isEmbeddedShellFocused,
}) => {
const isShellFocused = useShellFocusState();
const uiState = useUIState();
const [justNavigatedHistory, setJustNavigatedHistory] = useState(false);
const [escPressCount, setEscPressCount] = useState(0);
const [showEscapePrompt, setShowEscapePrompt] = useState(false);
@ -326,6 +329,14 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
return;
}
// Intercept feedback dialog option keys (1, 2, 3) when dialog is open
if (
uiState.isFeedbackDialogOpen &&
(FEEDBACK_DIALOG_KEYS as readonly string[]).includes(key.name)
) {
return;
}
// Reset ESC count and hide prompt on any non-ESC key
if (key.name !== 'escape') {
if (escPressCount > 0 || showEscapePrompt) {
@ -670,6 +681,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
recentPasteTime,
commandSearchActive,
commandSearchCompletion,
uiState,
],
);