diff --git a/packages/cli/src/i18n/locales/de.js b/packages/cli/src/i18n/locales/de.js
index 1dc124c3f..d6f53f65d 100644
--- a/packages/cli/src/i18n/locales/de.js
+++ b/packages/cli/src/i18n/locales/de.js
@@ -298,7 +298,9 @@ export default {
'How is Qwen doing this session? (optional)':
'Wie macht sich Qwen in dieser Sitzung? (optional)',
Bad: 'Schlecht',
+ Fine: 'In Ordnung',
Good: 'Gut',
+ Dismiss: 'Ignorieren',
'Not Sure Yet': 'Noch nicht sicher',
'Any other key': 'Beliebige andere Taste',
'Disable Loading Phrases': 'Ladesprüche deaktivieren',
diff --git a/packages/cli/src/i18n/locales/en.js b/packages/cli/src/i18n/locales/en.js
index 929ffc904..5e28a5e8e 100644
--- a/packages/cli/src/i18n/locales/en.js
+++ b/packages/cli/src/i18n/locales/en.js
@@ -315,7 +315,9 @@ export default {
'How is Qwen doing this session? (optional)':
'How is Qwen doing this session? (optional)',
Bad: 'Bad',
+ Fine: 'Fine',
Good: 'Good',
+ Dismiss: 'Dismiss',
'Not Sure Yet': 'Not Sure Yet',
'Any other key': 'Any other key',
'Disable Loading Phrases': 'Disable Loading Phrases',
diff --git a/packages/cli/src/i18n/locales/ru.js b/packages/cli/src/i18n/locales/ru.js
index c5108ec5d..76f5ef1a4 100644
--- a/packages/cli/src/i18n/locales/ru.js
+++ b/packages/cli/src/i18n/locales/ru.js
@@ -319,7 +319,9 @@ export default {
'How is Qwen doing this session? (optional)':
'Как дела у Qwen в этой сессии? (необязательно)',
Bad: 'Плохо',
+ Fine: 'Нормально',
Good: 'Хорошо',
+ Dismiss: 'Отклонить',
'Not Sure Yet': 'Пока не уверен',
'Any other key': 'Любая другая клавиша',
'Disable Loading Phrases': 'Отключить фразы при загрузке',
diff --git a/packages/cli/src/i18n/locales/zh.js b/packages/cli/src/i18n/locales/zh.js
index d6603207c..57a4cda60 100644
--- a/packages/cli/src/i18n/locales/zh.js
+++ b/packages/cli/src/i18n/locales/zh.js
@@ -305,7 +305,9 @@ export default {
'Enable User Feedback': '启用用户反馈',
'How is Qwen doing this session? (optional)': 'Qwen 这次表现如何?(可选)',
Bad: '不满意',
+ Fine: '还行',
Good: '满意',
+ Dismiss: '忽略',
'Not Sure Yet': '暂不评价',
'Any other key': '任意其他键',
'Disable Loading Phrases': '禁用加载短语',
diff --git a/packages/cli/src/ui/AppContainer.tsx b/packages/cli/src/ui/AppContainer.tsx
index c1bd7b80c..82ab66dd6 100644
--- a/packages/cli/src/ui/AppContainer.tsx
+++ b/packages/cli/src/ui/AppContainer.tsx
@@ -1326,6 +1326,7 @@ export const AppContainer = (props: AppContainerProps) => {
isFeedbackDialogOpen,
openFeedbackDialog,
closeFeedbackDialog,
+ temporaryCloseFeedbackDialog,
submitFeedback,
} = useFeedbackDialog({
config,
@@ -1571,6 +1572,7 @@ export const AppContainer = (props: AppContainerProps) => {
// Feedback dialog
openFeedbackDialog,
closeFeedbackDialog,
+ temporaryCloseFeedbackDialog,
submitFeedback,
}),
[
@@ -1611,6 +1613,7 @@ export const AppContainer = (props: AppContainerProps) => {
// Feedback dialog
openFeedbackDialog,
closeFeedbackDialog,
+ temporaryCloseFeedbackDialog,
submitFeedback,
],
);
diff --git a/packages/cli/src/ui/FeedbackDialog.tsx b/packages/cli/src/ui/FeedbackDialog.tsx
index 7791dfb88..ec2bf3c40 100644
--- a/packages/cli/src/ui/FeedbackDialog.tsx
+++ b/packages/cli/src/ui/FeedbackDialog.tsx
@@ -5,19 +5,21 @@ 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,
+export const FEEDBACK_OPTIONS = {
+ BAD: 1,
+ FINE: 2,
+ GOOD: 3,
+ DISMISS: 0,
} as const;
const FEEDBACK_OPTION_KEYS = {
- [FEEDBACK_OPTIONS.GOOD]: '1',
- [FEEDBACK_OPTIONS.BAD]: '2',
- [FEEDBACK_OPTIONS.NOT_SURE]: 'any',
+ [FEEDBACK_OPTIONS.BAD]: '1',
+ [FEEDBACK_OPTIONS.FINE]: '2',
+ [FEEDBACK_OPTIONS.GOOD]: '3',
+ [FEEDBACK_OPTIONS.DISMISS]: '0',
} as const;
-export const FEEDBACK_DIALOG_KEYS = ['1', '2'] as const;
+export const FEEDBACK_DIALOG_KEYS = ['1', '2', '3', '0'] as const;
export const FeedbackDialog: React.FC = () => {
const uiState = useUIState();
@@ -25,15 +27,19 @@ export const FeedbackDialog: React.FC = () => {
useKeypress(
(key) => {
- if (key.name === FEEDBACK_OPTION_KEYS[FEEDBACK_OPTIONS.GOOD]) {
- uiActions.submitFeedback(FEEDBACK_OPTIONS.GOOD);
- } else if (key.name === FEEDBACK_OPTION_KEYS[FEEDBACK_OPTIONS.BAD]) {
+ // Handle keys 0-3: permanent close with feedback/dismiss
+ if (key.name === FEEDBACK_OPTION_KEYS[FEEDBACK_OPTIONS.BAD]) {
uiActions.submitFeedback(FEEDBACK_OPTIONS.BAD);
+ } else if (key.name === FEEDBACK_OPTION_KEYS[FEEDBACK_OPTIONS.FINE]) {
+ uiActions.submitFeedback(FEEDBACK_OPTIONS.FINE);
+ } else if (key.name === FEEDBACK_OPTION_KEYS[FEEDBACK_OPTIONS.GOOD]) {
+ uiActions.submitFeedback(FEEDBACK_OPTIONS.GOOD);
+ } else if (key.name === FEEDBACK_OPTION_KEYS[FEEDBACK_OPTIONS.DISMISS]) {
+ uiActions.submitFeedback(FEEDBACK_OPTIONS.DISMISS);
} else {
- uiActions.submitFeedback(FEEDBACK_OPTIONS.NOT_SURE);
+ // Handle other keys: temporary close
+ uiActions.temporaryCloseFeedbackDialog();
}
-
- uiActions.closeFeedbackDialog();
},
{ isActive: uiState.isFeedbackDialogOpen },
);
@@ -45,16 +51,24 @@ export const FeedbackDialog: React.FC = () => {
{t('How is Qwen doing this session? (optional)')}
+ {FEEDBACK_OPTION_KEYS[FEEDBACK_OPTIONS.BAD]}:
+ {t('Bad')}
+
+
+ {FEEDBACK_OPTION_KEYS[FEEDBACK_OPTIONS.FINE]}:{' '}
+
+ {t('Fine')}
+
{FEEDBACK_OPTION_KEYS[FEEDBACK_OPTIONS.GOOD]}:{' '}
{t('Good')}
- {FEEDBACK_OPTION_KEYS[FEEDBACK_OPTIONS.BAD]}:
- {t('Bad')}
+
+ {FEEDBACK_OPTION_KEYS[FEEDBACK_OPTIONS.DISMISS]}:{' '}
+
+ {t('Dismiss')}
- {t('Any other key')}:
- {t('Not Sure Yet')}
);
diff --git a/packages/cli/src/ui/components/InputPrompt.test.tsx b/packages/cli/src/ui/components/InputPrompt.test.tsx
index de4cd1dee..584dc15f6 100644
--- a/packages/cli/src/ui/components/InputPrompt.test.tsx
+++ b/packages/cli/src/ui/components/InputPrompt.test.tsx
@@ -36,6 +36,11 @@ vi.mock('../utils/clipboardUtils.js');
vi.mock('../contexts/UIStateContext.js', () => ({
useUIState: vi.fn(() => ({ isFeedbackDialogOpen: false })),
}));
+vi.mock('../contexts/UIActionsContext.js', () => ({
+ useUIActions: vi.fn(() => ({
+ temporaryCloseFeedbackDialog: vi.fn(),
+ })),
+}));
const mockSlashCommands: SlashCommand[] = [
{
diff --git a/packages/cli/src/ui/components/InputPrompt.tsx b/packages/cli/src/ui/components/InputPrompt.tsx
index 1d46d03ab..0e3c43806 100644
--- a/packages/cli/src/ui/components/InputPrompt.tsx
+++ b/packages/cli/src/ui/components/InputPrompt.tsx
@@ -37,6 +37,7 @@ 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 { useUIActions } from '../contexts/UIActionsContext.js';
import { FEEDBACK_DIALOG_KEYS } from '../FeedbackDialog.js';
export interface InputPromptProps {
buffer: TextBuffer;
@@ -109,6 +110,7 @@ export const InputPrompt: React.FC = ({
}) => {
const isShellFocused = useShellFocusState();
const uiState = useUIState();
+ const uiActions = useUIActions();
const [justNavigatedHistory, setJustNavigatedHistory] = useState(false);
const [escPressCount, setEscPressCount] = useState(0);
const [showEscapePrompt, setShowEscapePrompt] = useState(false);
@@ -337,12 +339,16 @@ export const InputPrompt: React.FC = ({
return;
}
- // Intercept feedback dialog option keys (1, 2) when dialog is open
- if (
- uiState.isFeedbackDialogOpen &&
- (FEEDBACK_DIALOG_KEYS as readonly string[]).includes(key.name)
- ) {
- return;
+ // Handle feedback dialog keyboard interactions when dialog is open
+ if (uiState.isFeedbackDialogOpen) {
+ // If it's one of the feedback option keys (1-4), let FeedbackDialog handle it
+ if ((FEEDBACK_DIALOG_KEYS as readonly string[]).includes(key.name)) {
+ return;
+ } else {
+ // For any other key, close feedback dialog temporarily and continue with normal processing
+ uiActions.temporaryCloseFeedbackDialog();
+ // Continue processing the key for normal input handling
+ }
}
// Reset ESC count and hide prompt on any non-ESC key
@@ -712,6 +718,7 @@ export const InputPrompt: React.FC = ({
onToggleShortcuts,
showShortcuts,
uiState,
+ uiActions,
],
);
diff --git a/packages/cli/src/ui/contexts/UIActionsContext.tsx b/packages/cli/src/ui/contexts/UIActionsContext.tsx
index a1e7f3b35..17d74dd4e 100644
--- a/packages/cli/src/ui/contexts/UIActionsContext.tsx
+++ b/packages/cli/src/ui/contexts/UIActionsContext.tsx
@@ -71,6 +71,7 @@ export interface UIActions {
// Feedback dialog
openFeedbackDialog: () => void;
closeFeedbackDialog: () => void;
+ temporaryCloseFeedbackDialog: () => void;
submitFeedback: (rating: number) => void;
}
diff --git a/packages/cli/src/ui/hooks/useFeedbackDialog.ts b/packages/cli/src/ui/hooks/useFeedbackDialog.ts
index 18865b1f0..432d6d15a 100644
--- a/packages/cli/src/ui/hooks/useFeedbackDialog.ts
+++ b/packages/cli/src/ui/hooks/useFeedbackDialog.ts
@@ -15,6 +15,7 @@ import {
USER_SETTINGS_PATH,
} from '../../config/settings.js';
import type { SessionStatsState } from '../contexts/SessionContext.js';
+import { FEEDBACK_OPTIONS } from '../FeedbackDialog.js';
import stripJsonComments from 'strip-json-comments';
const FEEDBACK_SHOW_PROBABILITY = 0.25; // 25% probability of showing feedback dialog
@@ -96,37 +97,48 @@ export const useFeedbackDialog = ({
}: UseFeedbackDialogProps) => {
// Feedback dialog state
const [isFeedbackDialogOpen, setIsFeedbackDialogOpen] = useState(false);
+ const [isFeedbackDismissedTemporarily, setIsFeedbackDismissedTemporarily] =
+ useState(false);
const openFeedbackDialog = useCallback(() => {
setIsFeedbackDialogOpen(true);
-
- // Record the timestamp when feedback dialog is shown (fire and forget)
- settings.setValue(
- SettingScope.User,
- 'ui.feedbackLastShownTimestamp',
- Date.now(),
- );
- }, [settings]);
+ }, []);
const closeFeedbackDialog = useCallback(
() => setIsFeedbackDialogOpen(false),
[],
);
+ const temporaryCloseFeedbackDialog = useCallback(() => {
+ setIsFeedbackDialogOpen(false);
+ setIsFeedbackDismissedTemporarily(true);
+ }, []);
+
const submitFeedback = useCallback(
(rating: number) => {
- // Create and log the feedback event
- const feedbackEvent = new UserFeedbackEvent(
- sessionStats.sessionId,
- rating as UserFeedbackRating,
- config.getModel(),
- config.getApprovalMode(),
- );
+ // Only create and log feedback event for ratings 1-3 (BAD, FINE, GOOD)
+ // Rating 0 (DISMISS) should not trigger any telemetry
+ if (rating >= FEEDBACK_OPTIONS.BAD && rating <= FEEDBACK_OPTIONS.GOOD) {
+ const feedbackEvent = new UserFeedbackEvent(
+ sessionStats.sessionId,
+ rating as UserFeedbackRating,
+ config.getModel(),
+ config.getApprovalMode(),
+ );
+
+ logUserFeedback(config, feedbackEvent);
+
+ // Record the timestamp when feedback dialog is submitted
+ settings.setValue(
+ SettingScope.User,
+ 'ui.feedbackLastShownTimestamp',
+ Date.now(),
+ );
+ }
- logUserFeedback(config, feedbackEvent);
closeFeedbackDialog();
},
- [config, sessionStats, closeFeedbackDialog],
+ [closeFeedbackDialog, sessionStats.sessionId, config, settings],
);
useEffect(() => {
@@ -140,13 +152,15 @@ export const useFeedbackDialog = ({
// 5. Random chance (25% probability)
// 6. Meets minimum requirements (tool calls > 10 OR user messages > 5)
// 7. Fatigue mechanism allows showing (not shown recently across sessions)
+ // 8. Not temporarily dismissed
if (
config.getAuthType() !== AuthType.QWEN_OAUTH ||
!config.getUsageStatisticsEnabled() ||
settings.merged.ui?.enableUserFeedback === false ||
!lastMessageIsAIResponse(history) ||
Math.random() > FEEDBACK_SHOW_PROBABILITY ||
- !meetsMinimumSessionRequirements(sessionStats)
+ !meetsMinimumSessionRequirements(sessionStats) ||
+ isFeedbackDismissedTemporarily
) {
return;
}
@@ -164,15 +178,27 @@ export const useFeedbackDialog = ({
history,
sessionStats,
isFeedbackDialogOpen,
+ isFeedbackDismissedTemporarily,
openFeedbackDialog,
settings.merged.ui?.enableUserFeedback,
config,
]);
+ // Reset temporary dismissal when a new AI response starts streaming
+ useEffect(() => {
+ if (
+ streamingState === StreamingState.Responding &&
+ isFeedbackDismissedTemporarily
+ ) {
+ setIsFeedbackDismissedTemporarily(false);
+ }
+ }, [streamingState, isFeedbackDismissedTemporarily]);
+
return {
isFeedbackDialogOpen,
openFeedbackDialog,
closeFeedbackDialog,
+ temporaryCloseFeedbackDialog,
submitFeedback,
};
};