From f7585153b7c7b0e04c09155284aa685f1e1ec6bc Mon Sep 17 00:00:00 2001 From: DragonnZhang <731557579@qq.com> Date: Mon, 12 Jan 2026 11:15:43 +0800 Subject: [PATCH 01/10] feat: Add user feedback dialog --- packages/cli/src/config/settingsSchema.ts | 10 ++ packages/cli/src/i18n/locales/de.js | 7 + packages/cli/src/i18n/locales/en.js | 7 + packages/cli/src/i18n/locales/ru.js | 7 + packages/cli/src/i18n/locales/zh.js | 7 + packages/cli/src/ui/AppContainer.tsx | 136 +++++++++++++++++- packages/cli/src/ui/FeedbackDialog.tsx | 54 +++++++ .../cli/src/ui/components/DialogManager.tsx | 5 + .../cli/src/ui/contexts/UIActionsContext.tsx | 4 + .../cli/src/ui/contexts/UIStateContext.tsx | 2 + packages/core/src/telemetry/constants.ts | 1 + packages/core/src/telemetry/index.ts | 3 + packages/core/src/telemetry/loggers.ts | 31 ++++ .../src/telemetry/qwen-logger/qwen-logger.ts | 18 +++ packages/core/src/telemetry/types.ts | 41 +++++- 15 files changed, 331 insertions(+), 2 deletions(-) create mode 100644 packages/cli/src/ui/FeedbackDialog.tsx diff --git a/packages/cli/src/config/settingsSchema.ts b/packages/cli/src/config/settingsSchema.ts index 74b63a7b9..c98eb437d 100644 --- a/packages/cli/src/config/settingsSchema.ts +++ b/packages/cli/src/config/settingsSchema.ts @@ -434,6 +434,16 @@ const SETTINGS_SCHEMA = { 'Show welcome back dialog when returning to a project with conversation history.', showInDialog: true, }, + enableUserFeedback: { + type: 'boolean', + label: 'Enable User Feedback', + category: 'UI', + requiresRestart: false, + default: true, + description: + 'Show optional feedback dialog after conversations to help improve Qwen performance.', + showInDialog: true, + }, accessibility: { type: 'object', label: 'Accessibility', diff --git a/packages/cli/src/i18n/locales/de.js b/packages/cli/src/i18n/locales/de.js index fa4221854..475806390 100644 --- a/packages/cli/src/i18n/locales/de.js +++ b/packages/cli/src/i18n/locales/de.js @@ -289,6 +289,13 @@ export default { 'Show Citations': 'Quellenangaben anzeigen', 'Custom Witty Phrases': 'Benutzerdefinierte Witzige Sprüche', 'Enable Welcome Back': 'Willkommen-zurück aktivieren', + 'Enable User Feedback': 'Benutzerfeedback aktivieren', + 'How is Claude doing this session? (optional)': + 'Wie macht sich Claude in dieser Sitzung? (optional)', + Bad: 'Schlecht', + Fine: 'In Ordnung', + Good: 'Gut', + Dismiss: 'Verwerfen', 'Disable Loading Phrases': 'Ladesprüche deaktivieren', 'Screen Reader Mode': 'Bildschirmleser-Modus', 'IDE Mode': 'IDE-Modus', diff --git a/packages/cli/src/i18n/locales/en.js b/packages/cli/src/i18n/locales/en.js index 51461f4cb..a8d7578b3 100644 --- a/packages/cli/src/i18n/locales/en.js +++ b/packages/cli/src/i18n/locales/en.js @@ -286,6 +286,13 @@ export default { 'Show Citations': 'Show Citations', 'Custom Witty Phrases': 'Custom Witty Phrases', 'Enable Welcome Back': 'Enable Welcome Back', + 'Enable User Feedback': 'Enable User Feedback', + 'How is Claude doing this session? (optional)': + 'How is Claude doing this session? (optional)', + Bad: 'Bad', + Fine: 'Fine', + Good: 'Good', + Dismiss: 'Dismiss', 'Disable Loading Phrases': 'Disable Loading Phrases', 'Screen Reader Mode': 'Screen Reader Mode', 'IDE Mode': 'IDE Mode', diff --git a/packages/cli/src/i18n/locales/ru.js b/packages/cli/src/i18n/locales/ru.js index 82f2436ef..8f6dbdaf9 100644 --- a/packages/cli/src/i18n/locales/ru.js +++ b/packages/cli/src/i18n/locales/ru.js @@ -289,6 +289,13 @@ export default { 'Show Citations': 'Показывать цитаты', 'Custom Witty Phrases': 'Пользовательские остроумные фразы', 'Enable Welcome Back': 'Включить приветствие при возврате', + 'Enable User Feedback': 'Включить отзывы пользователей', + 'How is Claude doing this session? (optional)': + 'Как дела у Claude в этой сессии? (необязательно)', + Bad: 'Плохо', + Fine: 'Нормально', + Good: 'Хорошо', + Dismiss: 'Отклонить', 'Disable Loading Phrases': 'Отключить фразы при загрузке', 'Screen Reader Mode': 'Режим программы чтения с экрана', 'IDE Mode': 'Режим IDE', diff --git a/packages/cli/src/i18n/locales/zh.js b/packages/cli/src/i18n/locales/zh.js index a1b9c2033..77788d803 100644 --- a/packages/cli/src/i18n/locales/zh.js +++ b/packages/cli/src/i18n/locales/zh.js @@ -277,6 +277,13 @@ export default { 'Show Citations': '显示引用', 'Custom Witty Phrases': '自定义诙谐短语', 'Enable Welcome Back': '启用欢迎回来', + 'Enable User Feedback': '启用用户反馈', + 'How is Claude doing this session? (optional)': + 'Claude 这次表现如何?(可选)', + Bad: '差', + Fine: '一般', + Good: '好', + Dismiss: '忽略', 'Disable Loading Phrases': '禁用加载短语', 'Screen Reader Mode': '屏幕阅读器模式', 'IDE Mode': 'IDE 模式', diff --git a/packages/cli/src/ui/AppContainer.tsx b/packages/cli/src/ui/AppContainer.tsx index b10bbe1e7..46f847aa6 100644 --- a/packages/cli/src/ui/AppContainer.tsx +++ b/packages/cli/src/ui/AppContainer.tsx @@ -37,6 +37,9 @@ import { getErrorMessage, getAllGeminiMdFilenames, ShellExecutionService, + logUserFeedback, + UserFeedbackEvent, + type UserFeedbackRating, } from '@qwen-code/qwen-code-core'; import { buildResumedHistoryItems } from './utils/resumeHistoryUtils.js'; import { validateAuthMethod } from '../config/auth.js'; @@ -182,6 +185,18 @@ export const AppContainer = (props: AppContainerProps) => { // Helper to determine the current model (polled, since Config has no model-change event). const getCurrentModel = useCallback(() => config.getModel(), [config]); + // Feedback dialog state + const [isFeedbackDialogOpen, setIsFeedbackDialogOpen] = useState(false); + const [feedbackShownForSession, setFeedbackShownForSession] = useState(false); + const openFeedbackDialog = useCallback(() => { + setIsFeedbackDialogOpen(true); + setFeedbackShownForSession(true); + }, []); + const closeFeedbackDialog = useCallback( + () => setIsFeedbackDialogOpen(false), + [], + ); + const [currentModel, setCurrentModel] = useState(getCurrentModel()); const [isConfigInitialized, setConfigInitialized] = useState(false); @@ -198,6 +213,40 @@ export const AppContainer = (props: AppContainerProps) => { const logger = useLogger(config.storage, sessionStats.sessionId); const branchName = useGitBranchName(config.getTargetDir()); + // Submit user feedback function + const submitFeedback = useCallback( + (rating: number) => { + // Calculate session duration and turn count + const sessionDurationMs = + Date.now() - sessionStats.sessionStartTime.getTime(); + let lastUserMessageIndex = -1; + for (let i = historyManager.history.length - 1; i >= 0; i--) { + if (historyManager.history[i].type === MessageType.USER) { + lastUserMessageIndex = i; + break; + } + } + const turnCount = + lastUserMessageIndex === -1 + ? 0 + : historyManager.history.length - lastUserMessageIndex; + + // Create and log the feedback event + const feedbackEvent = new UserFeedbackEvent( + sessionStats.sessionId, + rating as UserFeedbackRating, + sessionDurationMs, + turnCount, + config.getModel(), + config.getApprovalMode(), + ); + + logUserFeedback(config, feedbackEvent); + closeFeedbackDialog(); + }, + [sessionStats, historyManager.history, config, closeFeedbackDialog], + ); + // Layout measurements const mainControlsRef = useRef(null); const originalTitleRef = useRef( @@ -1194,7 +1243,80 @@ export const AppContainer = (props: AppContainerProps) => { isSubagentCreateDialogOpen || isAgentsManagerDialogOpen || isApprovalModeDialogOpen || - isResumeDialogOpen; + isResumeDialogOpen || + isFeedbackDialogOpen; + + // Track when to show feedback dialog + useEffect(() => { + if ( + streamingState === StreamingState.Idle && + historyManager.history.length > 0 + ) { + // Find the last user message and check if there's AI response after it + let lastUserMessageIndex = -1; + let hasAIResponseAfterLastUser = false; + + for (let i = historyManager.history.length - 1; i >= 0; i--) { + if (historyManager.history[i].type === MessageType.USER) { + lastUserMessageIndex = i; + break; + } + } + + // Check if there's any AI response (GEMINI message) after the last user message + if (lastUserMessageIndex !== -1) { + for ( + let i = lastUserMessageIndex + 1; + i < historyManager.history.length; + i++ + ) { + if (historyManager.history[i].type === MessageType.GEMINI) { + hasAIResponseAfterLastUser = true; + break; + } + } + } + + const sessionDurationMs = + Date.now() - sessionStats.sessionStartTime.getTime(); + + // Show feedback dialog if: + // 1. Telemetry is enabled (required for feedback submission) + // 2. User feedback is enabled in settings + // 3. There's an AI response after the last user message (real AI conversation) + // 4. Session duration > 10 seconds (meaningful interaction) + // 5. No other dialogs are open + // 6. Not already shown for this session + // 7. Random chance (25% probability) + if ( + config.getUsageStatisticsEnabled() && // Only show if telemetry is enabled + settings.merged.ui?.enableUserFeedback !== false && // Default to true if not set + hasAIResponseAfterLastUser && + sessionDurationMs > 10000 && // 10 seconds minimum for meaningful interaction + !dialogsVisible && + !isFeedbackDialogOpen && + !feedbackShownForSession && + Math.random() < 0.25 // 25% probability + ) { + setTimeout(() => { + // Double check no dialogs opened in the meantime + if (!dialogsVisible && !isFeedbackDialogOpen) { + openFeedbackDialog(); + } + }, 1000); // Delay to ensure user has time to see the completion + } + } + }, [ + streamingState, + historyManager.history, + sessionStats, + dialogsVisible, + isFeedbackDialogOpen, + feedbackShownForSession, + openFeedbackDialog, + settings.merged.ui?.enableUserFeedback, + config, + ]); const pendingHistoryItems = useMemo( () => [...pendingSlashCommandHistoryItems, ...pendingGeminiHistoryItems], @@ -1292,6 +1414,8 @@ export const AppContainer = (props: AppContainerProps) => { // Subagent dialogs isSubagentCreateDialogOpen, isAgentsManagerDialogOpen, + // Feedback dialog + isFeedbackDialogOpen, }), [ isThemeDialogOpen, @@ -1382,6 +1506,8 @@ export const AppContainer = (props: AppContainerProps) => { // Subagent dialogs isSubagentCreateDialogOpen, isAgentsManagerDialogOpen, + // Feedback dialog + isFeedbackDialogOpen, ], ); @@ -1422,6 +1548,10 @@ export const AppContainer = (props: AppContainerProps) => { openResumeDialog, closeResumeDialog, handleResume, + // Feedback dialog + openFeedbackDialog, + closeFeedbackDialog, + submitFeedback, }), [ handleThemeSelect, @@ -1457,6 +1587,10 @@ export const AppContainer = (props: AppContainerProps) => { openResumeDialog, closeResumeDialog, handleResume, + // Feedback dialog + openFeedbackDialog, + closeFeedbackDialog, + submitFeedback, ], ); diff --git a/packages/cli/src/ui/FeedbackDialog.tsx b/packages/cli/src/ui/FeedbackDialog.tsx new file mode 100644 index 000000000..1b8847cfc --- /dev/null +++ b/packages/cli/src/ui/FeedbackDialog.tsx @@ -0,0 +1,54 @@ +import { Box, Text } from 'ink'; +import type React from 'react'; +import { t } from '../i18n/index.js'; +import { useUIActions } from './contexts/UIActionsContext.js'; +import { useUIState } from './contexts/UIStateContext.js'; +import { useKeypress } from './hooks/useKeypress.js'; + +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(); + } + }, + { isActive: uiState.isFeedbackDialogOpen }, + ); + + if (!uiState.isFeedbackDialogOpen) { + return null; + } + + return ( + + + + {t('How is Claude doing this session? (optional)')} + + + 1: + {t('Bad')} + + 2: + {t('Fine')} + + 3: + {t('Good')} + + 0: + {t('Dismiss')} + + + ); +}; diff --git a/packages/cli/src/ui/components/DialogManager.tsx b/packages/cli/src/ui/components/DialogManager.tsx index 6ff9f4aae..0df2d31f2 100644 --- a/packages/cli/src/ui/components/DialogManager.tsx +++ b/packages/cli/src/ui/components/DialogManager.tsx @@ -35,6 +35,7 @@ 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']; @@ -291,5 +292,9 @@ export const DialogManager = ({ ); } + if (uiState.isFeedbackDialogOpen) { + return ; + } + return null; }; diff --git a/packages/cli/src/ui/contexts/UIActionsContext.tsx b/packages/cli/src/ui/contexts/UIActionsContext.tsx index 93c0528d8..9a01a81fd 100644 --- a/packages/cli/src/ui/contexts/UIActionsContext.tsx +++ b/packages/cli/src/ui/contexts/UIActionsContext.tsx @@ -66,6 +66,10 @@ export interface UIActions { openResumeDialog: () => void; closeResumeDialog: () => void; handleResume: (sessionId: string) => void; + // Feedback dialog + openFeedbackDialog: () => void; + closeFeedbackDialog: () => void; + submitFeedback: (rating: number) => void; } export const UIActionsContext = createContext(null); diff --git a/packages/cli/src/ui/contexts/UIStateContext.tsx b/packages/cli/src/ui/contexts/UIStateContext.tsx index 806cf09ba..4cfc00bbc 100644 --- a/packages/cli/src/ui/contexts/UIStateContext.tsx +++ b/packages/cli/src/ui/contexts/UIStateContext.tsx @@ -126,6 +126,8 @@ export interface UIState { // Subagent dialogs isSubagentCreateDialogOpen: boolean; isAgentsManagerDialogOpen: boolean; + // Feedback dialog + isFeedbackDialogOpen: boolean; } export const UIStateContext = createContext(null); diff --git a/packages/core/src/telemetry/constants.ts b/packages/core/src/telemetry/constants.ts index acbf56025..66cdf1e49 100644 --- a/packages/core/src/telemetry/constants.ts +++ b/packages/core/src/telemetry/constants.ts @@ -35,6 +35,7 @@ export const EVENT_MODEL_SLASH_COMMAND = 'qwen-code.slash_command.model'; export const EVENT_SUBAGENT_EXECUTION = 'qwen-code.subagent_execution'; export const EVENT_SKILL_LAUNCH = 'qwen-code.skill_launch'; export const EVENT_AUTH = 'qwen-code.auth'; +export const EVENT_USER_FEEDBACK = 'qwen-code.user_feedback'; // Performance Events export const EVENT_STARTUP_PERFORMANCE = 'qwen-code.startup.performance'; diff --git a/packages/core/src/telemetry/index.ts b/packages/core/src/telemetry/index.ts index d0feb0202..0c2df012d 100644 --- a/packages/core/src/telemetry/index.ts +++ b/packages/core/src/telemetry/index.ts @@ -45,6 +45,7 @@ export { logNextSpeakerCheck, logAuth, logSkillLaunch, + logUserFeedback, } from './loggers.js'; export type { SlashCommandEvent, ChatCompressionEvent } from './types.js'; export { @@ -65,6 +66,8 @@ export { NextSpeakerCheckEvent, AuthEvent, SkillLaunchEvent, + UserFeedbackEvent, + UserFeedbackRating, } from './types.js'; export { makeSlashCommandEvent, makeChatCompressionEvent } from './types.js'; export type { TelemetryEvent } from './types.js'; diff --git a/packages/core/src/telemetry/loggers.ts b/packages/core/src/telemetry/loggers.ts index 16b4dc5a7..446acfda0 100644 --- a/packages/core/src/telemetry/loggers.ts +++ b/packages/core/src/telemetry/loggers.ts @@ -38,6 +38,7 @@ import { EVENT_INVALID_CHUNK, EVENT_AUTH, EVENT_SKILL_LAUNCH, + EVENT_USER_FEEDBACK, } from './constants.js'; import { recordApiErrorMetrics, @@ -86,6 +87,7 @@ import type { InvalidChunkEvent, AuthEvent, SkillLaunchEvent, + UserFeedbackEvent, } from './types.js'; import type { UiEvent } from './uiTelemetry.js'; import { uiTelemetryService } from './uiTelemetry.js'; @@ -887,3 +889,32 @@ export function logSkillLaunch(config: Config, event: SkillLaunchEvent): void { }; logger.emit(logRecord); } + +export function logUserFeedback( + config: Config, + event: UserFeedbackEvent, +): void { + const uiEvent = { + ...event, + 'event.name': EVENT_USER_FEEDBACK, + 'event.timestamp': new Date().toISOString(), + } as UiEvent; + uiTelemetryService.addEvent(uiEvent); + config.getChatRecordingService()?.recordUiTelemetryEvent(uiEvent); + QwenLogger.getInstance(config)?.logUserFeedbackEvent(event); + if (!isTelemetrySdkInitialized()) return; + + const attributes: LogAttributes = { + ...getCommonAttributes(config), + ...event, + 'event.name': EVENT_USER_FEEDBACK, + 'event.timestamp': new Date().toISOString(), + }; + + const logger = logs.getLogger(SERVICE_NAME); + const logRecord: LogRecord = { + body: `User feedback: Rating ${event.rating} for session ${event.session_id}. Turn count: ${event.turn_count}. Duration: ${event.session_duration_ms}ms.`, + attributes, + }; + logger.emit(logRecord); +} diff --git a/packages/core/src/telemetry/qwen-logger/qwen-logger.ts b/packages/core/src/telemetry/qwen-logger/qwen-logger.ts index e63511df8..76a63231c 100644 --- a/packages/core/src/telemetry/qwen-logger/qwen-logger.ts +++ b/packages/core/src/telemetry/qwen-logger/qwen-logger.ts @@ -39,6 +39,7 @@ import type { ExtensionDisableEvent, AuthEvent, SkillLaunchEvent, + UserFeedbackEvent, RipgrepFallbackEvent, EndSessionEvent, } from '../types.js'; @@ -842,6 +843,23 @@ export class QwenLogger { this.flushIfNeeded(); } + logUserFeedbackEvent(event: UserFeedbackEvent): void { + const rumEvent = this.createActionEvent('user', 'user_feedback', { + properties: { + session_id: event.session_id, + rating: event.rating, + session_duration_ms: event.session_duration_ms, + turn_count: event.turn_count, + model: event.model, + approval_mode: event.approval_mode, + prompt_id: event.prompt_id || '', + }, + }); + + this.enqueueLogEvent(rumEvent); + this.flushIfNeeded(); + } + logChatCompressionEvent(event: ChatCompressionEvent): void { const rumEvent = this.createActionEvent('misc', 'chat_compression', { properties: { diff --git a/packages/core/src/telemetry/types.ts b/packages/core/src/telemetry/types.ts index 4158c9053..528fd0200 100644 --- a/packages/core/src/telemetry/types.ts +++ b/packages/core/src/telemetry/types.ts @@ -757,6 +757,44 @@ export class SkillLaunchEvent implements BaseTelemetryEvent { } } +export enum UserFeedbackRating { + BAD = 1, + FINE = 2, + GOOD = 3, +} + +export class UserFeedbackEvent implements BaseTelemetryEvent { + 'event.name': 'user_feedback'; + 'event.timestamp': string; + session_id: string; + rating: UserFeedbackRating; + session_duration_ms: number; + turn_count: number; + model: string; + approval_mode: string; + prompt_id?: string; + + constructor( + session_id: string, + rating: UserFeedbackRating, + session_duration_ms: number, + turn_count: number, + model: string, + approval_mode: string, + prompt_id?: string, + ) { + this['event.name'] = 'user_feedback'; + this['event.timestamp'] = new Date().toISOString(); + this.session_id = session_id; + this.rating = rating; + this.session_duration_ms = session_duration_ms; + this.turn_count = turn_count; + this.model = model; + this.approval_mode = approval_mode; + this.prompt_id = prompt_id; + } +} + export type TelemetryEvent = | StartSessionEvent | EndSessionEvent @@ -786,7 +824,8 @@ export type TelemetryEvent = | ToolOutputTruncatedEvent | ModelSlashCommandEvent | AuthEvent - | SkillLaunchEvent; + | SkillLaunchEvent + | UserFeedbackEvent; export class ExtensionDisableEvent implements BaseTelemetryEvent { 'event.name': 'extension_disable'; From d095a8b3f1c96ecaefa651a7199e2a1bbfdc697b Mon Sep 17 00:00:00 2001 From: DragonnZhang <731557579@qq.com> Date: Mon, 12 Jan 2026 13:10:43 +0800 Subject: [PATCH 02/10] feat: Refactor feedback dialog logic into a custom hook --- packages/cli/src/ui/AppContainer.tsx | 135 ++-------------- .../cli/src/ui/hooks/useFeedbackDialog.ts | 145 ++++++++++++++++++ 2 files changed, 159 insertions(+), 121 deletions(-) create mode 100644 packages/cli/src/ui/hooks/useFeedbackDialog.ts diff --git a/packages/cli/src/ui/AppContainer.tsx b/packages/cli/src/ui/AppContainer.tsx index 46f847aa6..c4b39696c 100644 --- a/packages/cli/src/ui/AppContainer.tsx +++ b/packages/cli/src/ui/AppContainer.tsx @@ -37,9 +37,6 @@ import { getErrorMessage, getAllGeminiMdFilenames, ShellExecutionService, - logUserFeedback, - UserFeedbackEvent, - type UserFeedbackRating, } from '@qwen-code/qwen-code-core'; import { buildResumedHistoryItems } from './utils/resumeHistoryUtils.js'; import { validateAuthMethod } from '../config/auth.js'; @@ -48,6 +45,7 @@ import process from 'node:process'; import { useHistory } from './hooks/useHistoryManager.js'; import { useMemoryMonitor } from './hooks/useMemoryMonitor.js'; import { useThemeCommand } from './hooks/useThemeCommand.js'; +import { useFeedbackDialog } from './hooks/useFeedbackDialog.js'; import { useAuthCommand } from './auth/useAuth.js'; import { useEditorSettings } from './hooks/useEditorSettings.js'; import { useSettingsCommand } from './hooks/useSettingsCommand.js'; @@ -185,18 +183,6 @@ export const AppContainer = (props: AppContainerProps) => { // Helper to determine the current model (polled, since Config has no model-change event). const getCurrentModel = useCallback(() => config.getModel(), [config]); - // Feedback dialog state - const [isFeedbackDialogOpen, setIsFeedbackDialogOpen] = useState(false); - const [feedbackShownForSession, setFeedbackShownForSession] = useState(false); - const openFeedbackDialog = useCallback(() => { - setIsFeedbackDialogOpen(true); - setFeedbackShownForSession(true); - }, []); - const closeFeedbackDialog = useCallback( - () => setIsFeedbackDialogOpen(false), - [], - ); - const [currentModel, setCurrentModel] = useState(getCurrentModel()); const [isConfigInitialized, setConfigInitialized] = useState(false); @@ -213,40 +199,6 @@ export const AppContainer = (props: AppContainerProps) => { const logger = useLogger(config.storage, sessionStats.sessionId); const branchName = useGitBranchName(config.getTargetDir()); - // Submit user feedback function - const submitFeedback = useCallback( - (rating: number) => { - // Calculate session duration and turn count - const sessionDurationMs = - Date.now() - sessionStats.sessionStartTime.getTime(); - let lastUserMessageIndex = -1; - for (let i = historyManager.history.length - 1; i >= 0; i--) { - if (historyManager.history[i].type === MessageType.USER) { - lastUserMessageIndex = i; - break; - } - } - const turnCount = - lastUserMessageIndex === -1 - ? 0 - : historyManager.history.length - lastUserMessageIndex; - - // Create and log the feedback event - const feedbackEvent = new UserFeedbackEvent( - sessionStats.sessionId, - rating as UserFeedbackRating, - sessionDurationMs, - turnCount, - config.getModel(), - config.getApprovalMode(), - ); - - logUserFeedback(config, feedbackEvent); - closeFeedbackDialog(); - }, - [sessionStats, historyManager.history, config, closeFeedbackDialog], - ); - // Layout measurements const mainControlsRef = useRef(null); const originalTitleRef = useRef( @@ -1222,6 +1174,19 @@ export const AppContainer = (props: AppContainerProps) => { const nightly = props.version.includes('nightly'); + const { + isFeedbackDialogOpen, + openFeedbackDialog, + closeFeedbackDialog, + submitFeedback, + } = useFeedbackDialog({ + config, + settings, + streamingState, + history: historyManager.history, + sessionStats, + }); + const dialogsVisible = showWelcomeBackDialog || showWorkspaceMigrationDialog || @@ -1246,78 +1211,6 @@ export const AppContainer = (props: AppContainerProps) => { isResumeDialogOpen || isFeedbackDialogOpen; - // Track when to show feedback dialog - useEffect(() => { - if ( - streamingState === StreamingState.Idle && - historyManager.history.length > 0 - ) { - // Find the last user message and check if there's AI response after it - let lastUserMessageIndex = -1; - let hasAIResponseAfterLastUser = false; - - for (let i = historyManager.history.length - 1; i >= 0; i--) { - if (historyManager.history[i].type === MessageType.USER) { - lastUserMessageIndex = i; - break; - } - } - - // Check if there's any AI response (GEMINI message) after the last user message - if (lastUserMessageIndex !== -1) { - for ( - let i = lastUserMessageIndex + 1; - i < historyManager.history.length; - i++ - ) { - if (historyManager.history[i].type === MessageType.GEMINI) { - hasAIResponseAfterLastUser = true; - break; - } - } - } - - const sessionDurationMs = - Date.now() - sessionStats.sessionStartTime.getTime(); - - // Show feedback dialog if: - // 1. Telemetry is enabled (required for feedback submission) - // 2. User feedback is enabled in settings - // 3. There's an AI response after the last user message (real AI conversation) - // 4. Session duration > 10 seconds (meaningful interaction) - // 5. No other dialogs are open - // 6. Not already shown for this session - // 7. Random chance (25% probability) - if ( - config.getUsageStatisticsEnabled() && // Only show if telemetry is enabled - settings.merged.ui?.enableUserFeedback !== false && // Default to true if not set - hasAIResponseAfterLastUser && - sessionDurationMs > 10000 && // 10 seconds minimum for meaningful interaction - !dialogsVisible && - !isFeedbackDialogOpen && - !feedbackShownForSession && - Math.random() < 0.25 // 25% probability - ) { - setTimeout(() => { - // Double check no dialogs opened in the meantime - if (!dialogsVisible && !isFeedbackDialogOpen) { - openFeedbackDialog(); - } - }, 1000); // Delay to ensure user has time to see the completion - } - } - }, [ - streamingState, - historyManager.history, - sessionStats, - dialogsVisible, - isFeedbackDialogOpen, - feedbackShownForSession, - openFeedbackDialog, - settings.merged.ui?.enableUserFeedback, - config, - ]); - const pendingHistoryItems = useMemo( () => [...pendingSlashCommandHistoryItems, ...pendingGeminiHistoryItems], [pendingSlashCommandHistoryItems, pendingGeminiHistoryItems], diff --git a/packages/cli/src/ui/hooks/useFeedbackDialog.ts b/packages/cli/src/ui/hooks/useFeedbackDialog.ts new file mode 100644 index 000000000..6c4d356b2 --- /dev/null +++ b/packages/cli/src/ui/hooks/useFeedbackDialog.ts @@ -0,0 +1,145 @@ +import { useState, useCallback, useEffect } from 'react'; +import { + type Config, + logUserFeedback, + UserFeedbackEvent, + type UserFeedbackRating, +} from '@qwen-code/qwen-code-core'; +import { StreamingState, MessageType, type HistoryItem } from '../types.js'; +import type { LoadedSettings } from '../../config/settings.js'; +import type { SessionStatsState } from '../contexts/SessionContext.js'; + +export interface UseFeedbackDialogProps { + config: Config; + settings: LoadedSettings; + streamingState: StreamingState; + history: HistoryItem[]; + sessionStats: SessionStatsState; +} + +export const useFeedbackDialog = ({ + config, + settings, + streamingState, + history, + sessionStats, +}: UseFeedbackDialogProps) => { + // Feedback dialog state + const [isFeedbackDialogOpen, setIsFeedbackDialogOpen] = useState(false); + const [feedbackShownForSession, setFeedbackShownForSession] = useState(false); + + const openFeedbackDialog = useCallback(() => { + setIsFeedbackDialogOpen(true); + setFeedbackShownForSession(true); + }, []); + + const closeFeedbackDialog = useCallback( + () => setIsFeedbackDialogOpen(false), + [], + ); + + const submitFeedback = useCallback( + (rating: number) => { + // Calculate session duration and turn count + const sessionDurationMs = + Date.now() - sessionStats.sessionStartTime.getTime(); + let lastUserMessageIndex = -1; + for (let i = history.length - 1; i >= 0; i--) { + if (history[i].type === MessageType.USER) { + lastUserMessageIndex = i; + break; + } + } + const turnCount = + lastUserMessageIndex === -1 ? 0 : history.length - lastUserMessageIndex; + + // Create and log the feedback event + const feedbackEvent = new UserFeedbackEvent( + sessionStats.sessionId, + rating as UserFeedbackRating, + sessionDurationMs, + turnCount, + config.getModel(), + config.getApprovalMode(), + ); + + logUserFeedback(config, feedbackEvent); + closeFeedbackDialog(); + }, + [config, sessionStats, history, closeFeedbackDialog], + ); + + // Track when to show feedback dialog + useEffect(() => { + let timeoutId: NodeJS.Timeout; + + if (streamingState === StreamingState.Idle && history.length > 0) { + // Find the last user message and check if there's AI response after it + let lastUserMessageIndex = -1; + let hasAIResponseAfterLastUser = false; + + for (let i = history.length - 1; i >= 0; i--) { + if (history[i].type === MessageType.USER) { + lastUserMessageIndex = i; + break; + } + } + + // Check if there's any AI response (GEMINI message) after the last user message + if (lastUserMessageIndex !== -1) { + for (let i = lastUserMessageIndex + 1; i < history.length; i++) { + if (history[i].type === MessageType.GEMINI) { + hasAIResponseAfterLastUser = true; + break; + } + } + } + + const sessionDurationMs = + Date.now() - sessionStats.sessionStartTime.getTime(); + + // Show feedback dialog if: + // 1. Telemetry is enabled (required for feedback submission) + // 2. User feedback is enabled in settings + // 3. There's an AI response after the last user message (real AI conversation) + // 4. Session duration > 10 seconds (meaningful interaction) + // 5. Not already shown for this session + // 6. Random chance (25% probability) + // Note: We check !isFeedbackDialogOpen to ensure it's not already open + if ( + config.getUsageStatisticsEnabled() && // Only show if telemetry is enabled + settings.merged.ui?.enableUserFeedback !== false && // Default to true if not set + hasAIResponseAfterLastUser && + sessionDurationMs > 10000 && // 10 seconds minimum for meaningful interaction + !feedbackShownForSession && + Math.random() < 0.25 // 25% probability + ) { + timeoutId = setTimeout(() => { + openFeedbackDialog(); + }, 1000); // Delay to ensure user has time to see the completion + } + } + + return () => { + if (timeoutId) { + clearTimeout(timeoutId); + } + }; + }, [ + streamingState, + history, + sessionStats, + isFeedbackDialogOpen, + feedbackShownForSession, + openFeedbackDialog, + settings.merged.ui?.enableUserFeedback, + config, + ]); + + return { + isFeedbackDialogOpen, + openFeedbackDialog, + closeFeedbackDialog, + submitFeedback, + }; +}; From e748532e6d486abd1aa55222a67de6e2e2881236 Mon Sep 17 00:00:00 2001 From: DragonnZhang <731557579@qq.com> Date: Mon, 12 Jan 2026 16:21:26 +0800 Subject: [PATCH 03/10] feat: Update feedback dialog text to reference Qwen instead of Claude --- packages/cli/src/i18n/locales/de.js | 4 +- packages/cli/src/i18n/locales/en.js | 4 +- packages/cli/src/i18n/locales/ru.js | 4 +- packages/cli/src/i18n/locales/zh.js | 3 +- packages/cli/src/ui/FeedbackDialog.tsx | 2 +- .../cli/src/ui/hooks/useFeedbackDialog.ts | 52 +++++++++++-------- 6 files changed, 38 insertions(+), 31 deletions(-) diff --git a/packages/cli/src/i18n/locales/de.js b/packages/cli/src/i18n/locales/de.js index 475806390..514ee134f 100644 --- a/packages/cli/src/i18n/locales/de.js +++ b/packages/cli/src/i18n/locales/de.js @@ -290,8 +290,8 @@ export default { 'Custom Witty Phrases': 'Benutzerdefinierte Witzige Sprüche', 'Enable Welcome Back': 'Willkommen-zurück aktivieren', 'Enable User Feedback': 'Benutzerfeedback aktivieren', - 'How is Claude doing this session? (optional)': - 'Wie macht sich Claude in dieser Sitzung? (optional)', + 'How is Qwen doing this session? (optional)': + 'Wie macht sich Qwen in dieser Sitzung? (optional)', Bad: 'Schlecht', Fine: 'In Ordnung', Good: 'Gut', diff --git a/packages/cli/src/i18n/locales/en.js b/packages/cli/src/i18n/locales/en.js index a8d7578b3..773f11b6e 100644 --- a/packages/cli/src/i18n/locales/en.js +++ b/packages/cli/src/i18n/locales/en.js @@ -287,8 +287,8 @@ export default { 'Custom Witty Phrases': 'Custom Witty Phrases', 'Enable Welcome Back': 'Enable Welcome Back', 'Enable User Feedback': 'Enable User Feedback', - 'How is Claude doing this session? (optional)': - 'How is Claude doing this session? (optional)', + 'How is Qwen doing this session? (optional)': + 'How is Qwen doing this session? (optional)', Bad: 'Bad', Fine: 'Fine', Good: 'Good', diff --git a/packages/cli/src/i18n/locales/ru.js b/packages/cli/src/i18n/locales/ru.js index 8f6dbdaf9..33c4a2f00 100644 --- a/packages/cli/src/i18n/locales/ru.js +++ b/packages/cli/src/i18n/locales/ru.js @@ -290,8 +290,8 @@ export default { 'Custom Witty Phrases': 'Пользовательские остроумные фразы', 'Enable Welcome Back': 'Включить приветствие при возврате', 'Enable User Feedback': 'Включить отзывы пользователей', - 'How is Claude doing this session? (optional)': - 'Как дела у Claude в этой сессии? (необязательно)', + 'How is Qwen doing this session? (optional)': + 'Как дела у Qwen в этой сессии? (необязательно)', Bad: 'Плохо', Fine: 'Нормально', Good: 'Хорошо', diff --git a/packages/cli/src/i18n/locales/zh.js b/packages/cli/src/i18n/locales/zh.js index 77788d803..7be23ef48 100644 --- a/packages/cli/src/i18n/locales/zh.js +++ b/packages/cli/src/i18n/locales/zh.js @@ -278,8 +278,7 @@ export default { 'Custom Witty Phrases': '自定义诙谐短语', 'Enable Welcome Back': '启用欢迎回来', 'Enable User Feedback': '启用用户反馈', - 'How is Claude doing this session? (optional)': - 'Claude 这次表现如何?(可选)', + 'How is Qwen doing this session? (optional)': 'Qwen 这次表现如何?(可选)', Bad: '差', Fine: '一般', Good: '好', diff --git a/packages/cli/src/ui/FeedbackDialog.tsx b/packages/cli/src/ui/FeedbackDialog.tsx index 1b8847cfc..346d3f1ac 100644 --- a/packages/cli/src/ui/FeedbackDialog.tsx +++ b/packages/cli/src/ui/FeedbackDialog.tsx @@ -34,7 +34,7 @@ export const FeedbackDialog: React.FC = () => { - {t('How is Claude doing this session? (optional)')} + {t('How is Qwen doing this session? (optional)')} 1: diff --git a/packages/cli/src/ui/hooks/useFeedbackDialog.ts b/packages/cli/src/ui/hooks/useFeedbackDialog.ts index 6c4d356b2..cee4d8c2a 100644 --- a/packages/cli/src/ui/hooks/useFeedbackDialog.ts +++ b/packages/cli/src/ui/hooks/useFeedbackDialog.ts @@ -9,6 +9,33 @@ import { StreamingState, MessageType, type HistoryItem } from '../types.js'; import type { LoadedSettings } from '../../config/settings.js'; import type { SessionStatsState } from '../contexts/SessionContext.js'; +const FEEDBACK_SHOW_PROBABILITY = 0.25; // 25% probability of showing feedback dialog + +/** + * Check if there's an AI response after the last user message in the conversation history + */ +const hasAIResponseAfterLastUserMessage = (history: HistoryItem[]): boolean => { + // Find the last user message + let lastUserMessageIndex = -1; + for (let i = history.length - 1; i >= 0; i--) { + if (history[i].type === MessageType.USER) { + lastUserMessageIndex = i; + break; + } + } + + // Check if there's any AI response (GEMINI message) after the last user message + if (lastUserMessageIndex !== -1) { + for (let i = lastUserMessageIndex + 1; i < history.length; i++) { + if (history[i].type === MessageType.GEMINI) { + return true; + } + } + } + + return false; +}; + export interface UseFeedbackDialogProps { config: Config; settings: LoadedSettings; @@ -74,26 +101,8 @@ export const useFeedbackDialog = ({ let timeoutId: NodeJS.Timeout; if (streamingState === StreamingState.Idle && history.length > 0) { - // Find the last user message and check if there's AI response after it - let lastUserMessageIndex = -1; - let hasAIResponseAfterLastUser = false; - - for (let i = history.length - 1; i >= 0; i--) { - if (history[i].type === MessageType.USER) { - lastUserMessageIndex = i; - break; - } - } - - // Check if there's any AI response (GEMINI message) after the last user message - if (lastUserMessageIndex !== -1) { - for (let i = lastUserMessageIndex + 1; i < history.length; i++) { - if (history[i].type === MessageType.GEMINI) { - hasAIResponseAfterLastUser = true; - break; - } - } - } + const hasAIResponseAfterLastUser = + hasAIResponseAfterLastUserMessage(history); const sessionDurationMs = Date.now() - sessionStats.sessionStartTime.getTime(); @@ -105,14 +114,13 @@ export const useFeedbackDialog = ({ // 4. Session duration > 10 seconds (meaningful interaction) // 5. Not already shown for this session // 6. Random chance (25% probability) - // Note: We check !isFeedbackDialogOpen to ensure it's not already open if ( config.getUsageStatisticsEnabled() && // Only show if telemetry is enabled settings.merged.ui?.enableUserFeedback !== false && // Default to true if not set hasAIResponseAfterLastUser && sessionDurationMs > 10000 && // 10 seconds minimum for meaningful interaction !feedbackShownForSession && - Math.random() < 0.25 // 25% probability + Math.random() < FEEDBACK_SHOW_PROBABILITY ) { timeoutId = setTimeout(() => { openFeedbackDialog(); From 56391b11ad51cd27dc7e2561d55e5ad6657befd7 Mon Sep 17 00:00:00 2001 From: DragonnZhang <731557579@qq.com> Date: Tue, 13 Jan 2026 18:09:42 +0800 Subject: [PATCH 04/10] feat: Update feedback options in multiple languages and adjust dialog text --- packages/cli/src/i18n/locales/de.js | 3 +-- packages/cli/src/i18n/locales/en.js | 3 +-- packages/cli/src/i18n/locales/ru.js | 3 +-- packages/cli/src/i18n/locales/zh.js | 7 +++---- packages/cli/src/ui/FeedbackDialog.tsx | 13 +++++-------- 5 files changed, 11 insertions(+), 18 deletions(-) diff --git a/packages/cli/src/i18n/locales/de.js b/packages/cli/src/i18n/locales/de.js index 514ee134f..ec6d6a8e6 100644 --- a/packages/cli/src/i18n/locales/de.js +++ b/packages/cli/src/i18n/locales/de.js @@ -293,9 +293,8 @@ 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: 'Verwerfen', + 'Not Sure Yet': 'Noch nicht sicher', 'Disable Loading Phrases': 'Ladesprüche deaktivieren', 'Screen Reader Mode': 'Bildschirmleser-Modus', 'IDE Mode': 'IDE-Modus', diff --git a/packages/cli/src/i18n/locales/en.js b/packages/cli/src/i18n/locales/en.js index 773f11b6e..934e97868 100644 --- a/packages/cli/src/i18n/locales/en.js +++ b/packages/cli/src/i18n/locales/en.js @@ -290,9 +290,8 @@ 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', 'Disable Loading Phrases': 'Disable Loading Phrases', 'Screen Reader Mode': 'Screen Reader Mode', 'IDE Mode': 'IDE Mode', diff --git a/packages/cli/src/i18n/locales/ru.js b/packages/cli/src/i18n/locales/ru.js index 33c4a2f00..857aa1821 100644 --- a/packages/cli/src/i18n/locales/ru.js +++ b/packages/cli/src/i18n/locales/ru.js @@ -293,9 +293,8 @@ export default { 'How is Qwen doing this session? (optional)': 'Как дела у Qwen в этой сессии? (необязательно)', Bad: 'Плохо', - Fine: 'Нормально', Good: 'Хорошо', - Dismiss: 'Отклонить', + 'Not Sure Yet': 'Пока не уверен', 'Disable Loading Phrases': 'Отключить фразы при загрузке', 'Screen Reader Mode': 'Режим программы чтения с экрана', 'IDE Mode': 'Режим IDE', diff --git a/packages/cli/src/i18n/locales/zh.js b/packages/cli/src/i18n/locales/zh.js index 7be23ef48..e659da0a3 100644 --- a/packages/cli/src/i18n/locales/zh.js +++ b/packages/cli/src/i18n/locales/zh.js @@ -279,10 +279,9 @@ export default { 'Enable Welcome Back': '启用欢迎回来', 'Enable User Feedback': '启用用户反馈', 'How is Qwen doing this session? (optional)': 'Qwen 这次表现如何?(可选)', - Bad: '差', - Fine: '一般', - Good: '好', - Dismiss: '忽略', + Bad: '不满意', + Good: '满意', + 'Not Sure Yet': '暂不评价', 'Disable Loading Phrases': '禁用加载短语', 'Screen Reader Mode': '屏幕阅读器模式', 'IDE Mode': 'IDE 模式', diff --git a/packages/cli/src/ui/FeedbackDialog.tsx b/packages/cli/src/ui/FeedbackDialog.tsx index 346d3f1ac..187cde7d6 100644 --- a/packages/cli/src/ui/FeedbackDialog.tsx +++ b/packages/cli/src/ui/FeedbackDialog.tsx @@ -38,16 +38,13 @@ export const FeedbackDialog: React.FC = () => { 1: - {t('Bad')} - - 2: - {t('Fine')} - - 3: {t('Good')} - 0: - {t('Dismiss')} + 2: + {t('Bad')} + + 3: + {t('Not Sure Yet')} ); From 932572181154499714fed905ddf9871ee98b3c5b Mon Sep 17 00:00:00 2001 From: DragonnZhang <731557579@qq.com> Date: Tue, 13 Jan 2026 18:10:14 +0800 Subject: [PATCH 05/10] feat: Add minimum requirements for showing feedback dialog based on tool calls and user messages --- .../cli/src/ui/hooks/useFeedbackDialog.ts | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/ui/hooks/useFeedbackDialog.ts b/packages/cli/src/ui/hooks/useFeedbackDialog.ts index cee4d8c2a..cc005e044 100644 --- a/packages/cli/src/ui/hooks/useFeedbackDialog.ts +++ b/packages/cli/src/ui/hooks/useFeedbackDialog.ts @@ -10,6 +10,8 @@ import type { LoadedSettings } from '../../config/settings.js'; import type { SessionStatsState } from '../contexts/SessionContext.js'; const FEEDBACK_SHOW_PROBABILITY = 0.25; // 25% probability of showing feedback dialog +const MIN_TOOL_CALLS = 10; // Minimum tool calls to show feedback dialog +const MIN_USER_MESSAGES = 5; // Minimum user messages to show feedback dialog /** * Check if there's an AI response after the last user message in the conversation history @@ -36,6 +38,12 @@ const hasAIResponseAfterLastUserMessage = (history: HistoryItem[]): boolean => { return false; }; +/** + * Count the number of user messages in the conversation history + */ +const countUserMessages = (history: HistoryItem[]): number => + history.filter((item) => item.type === MessageType.USER).length; + export interface UseFeedbackDialogProps { config: Config; settings: LoadedSettings; @@ -107,6 +115,16 @@ export const useFeedbackDialog = ({ const sessionDurationMs = Date.now() - sessionStats.sessionStartTime.getTime(); + // Get tool calls count and user messages count + const toolCallsCount = sessionStats.metrics.tools.totalCalls; + const userMessagesCount = countUserMessages(history); + + // Check if the session meets the minimum requirements: + // Either tool calls > 10 OR user messages > 5 + const meetsMinimumRequirements = + toolCallsCount > MIN_TOOL_CALLS || + userMessagesCount > MIN_USER_MESSAGES; + // Show feedback dialog if: // 1. Telemetry is enabled (required for feedback submission) // 2. User feedback is enabled in settings @@ -114,13 +132,15 @@ export const useFeedbackDialog = ({ // 4. Session duration > 10 seconds (meaningful interaction) // 5. Not already shown for this session // 6. Random chance (25% probability) + // 7. Meets minimum requirements (tool calls > 10 OR user messages > 5) if ( config.getUsageStatisticsEnabled() && // Only show if telemetry is enabled settings.merged.ui?.enableUserFeedback !== false && // Default to true if not set hasAIResponseAfterLastUser && sessionDurationMs > 10000 && // 10 seconds minimum for meaningful interaction !feedbackShownForSession && - Math.random() < FEEDBACK_SHOW_PROBABILITY + Math.random() < FEEDBACK_SHOW_PROBABILITY && + meetsMinimumRequirements ) { timeoutId = setTimeout(() => { openFeedbackDialog(); From d91e372c72e8e8847cf74bd48a19b645df7e2f0e Mon Sep 17 00:00:00 2001 From: DragonnZhang <731557579@qq.com> Date: Wed, 14 Jan 2026 11:48:03 +0800 Subject: [PATCH 06/10] feat: Refactor feedback dialog to a non-blocking popup, allow user input while it is rendered --- packages/cli/src/ui/AppContainer.tsx | 3 +- packages/cli/src/ui/FeedbackDialog.tsx | 46 ++++++++++++------- packages/cli/src/ui/components/Composer.tsx | 3 ++ .../cli/src/ui/components/DialogManager.tsx | 5 -- .../src/ui/components/InputPrompt.test.tsx | 3 ++ .../cli/src/ui/components/InputPrompt.tsx | 12 +++++ 6 files changed, 48 insertions(+), 24 deletions(-) diff --git a/packages/cli/src/ui/AppContainer.tsx b/packages/cli/src/ui/AppContainer.tsx index c4b39696c..3c6f829c9 100644 --- a/packages/cli/src/ui/AppContainer.tsx +++ b/packages/cli/src/ui/AppContainer.tsx @@ -1208,8 +1208,7 @@ export const AppContainer = (props: AppContainerProps) => { isSubagentCreateDialogOpen || isAgentsManagerDialogOpen || isApprovalModeDialogOpen || - isResumeDialogOpen || - isFeedbackDialogOpen; + isResumeDialogOpen; const pendingHistoryItems = useMemo( () => [...pendingSlashCommandHistoryItems, ...pendingGeminiHistoryItems], diff --git a/packages/cli/src/ui/FeedbackDialog.tsx b/packages/cli/src/ui/FeedbackDialog.tsx index 187cde7d6..3984664d6 100644 --- a/packages/cli/src/ui/FeedbackDialog.tsx +++ b/packages/cli/src/ui/FeedbackDialog.tsx @@ -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 ( @@ -37,13 +45,17 @@ export const FeedbackDialog: React.FC = () => { {t('How is Qwen doing this session? (optional)')} - 1: + + {FEEDBACK_OPTION_KEYS[FEEDBACK_OPTIONS.GOOD]}:{' '} + {t('Good')} - 2: + {FEEDBACK_OPTION_KEYS[FEEDBACK_OPTIONS.BAD]}: {t('Bad')} - 3: + + {FEEDBACK_OPTION_KEYS[FEEDBACK_OPTIONS.NOT_SURE]}:{' '} + {t('Not Sure Yet')} diff --git a/packages/cli/src/ui/components/Composer.tsx b/packages/cli/src/ui/components/Composer.tsx index 1b51227a1..9052e4f4d 100644 --- a/packages/cli/src/ui/components/Composer.tsx +++ b/packages/cli/src/ui/components/Composer.tsx @@ -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 = () => { )} + {uiState.isFeedbackDialogOpen && } + {uiState.isInputActive && ( ; - } - return null; }; diff --git a/packages/cli/src/ui/components/InputPrompt.test.tsx b/packages/cli/src/ui/components/InputPrompt.test.tsx index cf8b9685c..c34578294 100644 --- a/packages/cli/src/ui/components/InputPrompt.test.tsx +++ b/packages/cli/src/ui/components/InputPrompt.test.tsx @@ -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[] = [ { diff --git a/packages/cli/src/ui/components/InputPrompt.tsx b/packages/cli/src/ui/components/InputPrompt.tsx index 7d1742505..1f7ed099a 100644 --- a/packages/cli/src/ui/components/InputPrompt.tsx +++ b/packages/cli/src/ui/components/InputPrompt.tsx @@ -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 = ({ 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 = ({ 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 = ({ recentPasteTime, commandSearchActive, commandSearchCompletion, + uiState, ], ); From 9e8724a74952f9ac30d053aca82d37254d328ddf Mon Sep 17 00:00:00 2001 From: DragonnZhang <731557579@qq.com> Date: Wed, 14 Jan 2026 14:10:40 +0800 Subject: [PATCH 07/10] feat: Implement feedback history management with fatigue mechanism --- .../cli/src/ui/hooks/useFeedbackDialog.ts | 162 +++++++++++++----- 1 file changed, 122 insertions(+), 40 deletions(-) diff --git a/packages/cli/src/ui/hooks/useFeedbackDialog.ts b/packages/cli/src/ui/hooks/useFeedbackDialog.ts index cc005e044..bfc5e82ca 100644 --- a/packages/cli/src/ui/hooks/useFeedbackDialog.ts +++ b/packages/cli/src/ui/hooks/useFeedbackDialog.ts @@ -1,9 +1,13 @@ import { useState, useCallback, useEffect } from 'react'; +import * as fs from 'node:fs/promises'; +import * as path from 'node:path'; import { type Config, logUserFeedback, UserFeedbackEvent, type UserFeedbackRating, + Storage, + isNodeError, } from '@qwen-code/qwen-code-core'; import { StreamingState, MessageType, type HistoryItem } from '../types.js'; import type { LoadedSettings } from '../../config/settings.js'; @@ -13,6 +17,10 @@ const FEEDBACK_SHOW_PROBABILITY = 0.25; // 25% probability of showing feedback d const MIN_TOOL_CALLS = 10; // Minimum tool calls to show feedback dialog const MIN_USER_MESSAGES = 5; // Minimum user messages to show feedback dialog +// Fatigue mechanism constants +const FEEDBACK_COOLDOWN_HOURS = 24; // Hours to wait before showing feedback dialog again +const FEEDBACK_HISTORY_FILENAME = 'feedback-history.json'; + /** * Check if there's an AI response after the last user message in the conversation history */ @@ -44,6 +52,70 @@ const hasAIResponseAfterLastUserMessage = (history: HistoryItem[]): boolean => { const countUserMessages = (history: HistoryItem[]): number => history.filter((item) => item.type === MessageType.USER).length; +/** + * Interface for feedback history storage + */ +interface FeedbackHistory { + lastShownTimestamp: number; +} + +/** + * Get the feedback history file path using global Storage + */ +function getFeedbackHistoryPath(): string { + const globalQwenDir = Storage.getGlobalQwenDir(); + return path.join(globalQwenDir, FEEDBACK_HISTORY_FILENAME); +} + +/** + * Get the last feedback dialog show time from file storage + */ +const getFeedbackHistory = async (): Promise => { + try { + const filePath = getFeedbackHistoryPath(); + const content = await fs.readFile(filePath, 'utf-8'); + return JSON.parse(content) as FeedbackHistory; + } catch (error) { + if (isNodeError(error) && error.code === 'ENOENT') { + // File doesn't exist yet, which is normal for first time + return null; + } + console.warn('Failed to read feedback history from file:', error); + return null; + } +}; + +/** + * Save feedback history to file storage + */ +const saveFeedbackHistory = async (history: FeedbackHistory): Promise => { + try { + const filePath = getFeedbackHistoryPath(); + + // Ensure the directory exists + await fs.mkdir(path.dirname(filePath), { recursive: true }); + + // Write the history file + await fs.writeFile(filePath, JSON.stringify(history, null, 2), 'utf-8'); + } catch (error) { + console.warn('Failed to save feedback history to file:', error); + } +}; + +/** + * Check if we should show the feedback dialog based on fatigue mechanism + */ +const shouldShowFeedbackBasedOnFatigue = async (): Promise => { + const history = await getFeedbackHistory(); + if (!history) return true; // No history, allow showing + + const now = Date.now(); + const timeSinceLastShown = now - history.lastShownTimestamp; + const cooldownMs = FEEDBACK_COOLDOWN_HOURS * 60 * 60 * 1000; + + return timeSinceLastShown >= cooldownMs; +}; + export interface UseFeedbackDialogProps { config: Config; settings: LoadedSettings; @@ -66,6 +138,11 @@ export const useFeedbackDialog = ({ const openFeedbackDialog = useCallback(() => { setIsFeedbackDialogOpen(true); setFeedbackShownForSession(true); + + // Record the timestamp when feedback dialog is shown (fire and forget) + saveFeedbackHistory({ + lastShownTimestamp: Date.now(), + }); }, []); const closeFeedbackDialog = useCallback( @@ -104,55 +181,60 @@ export const useFeedbackDialog = ({ [config, sessionStats, history, closeFeedbackDialog], ); - // Track when to show feedback dialog useEffect(() => { - let timeoutId: NodeJS.Timeout; + const checkAndShowFeedback = async () => { + if (streamingState === StreamingState.Idle && history.length > 0) { + const hasAIResponseAfterLastUser = + hasAIResponseAfterLastUserMessage(history); - if (streamingState === StreamingState.Idle && history.length > 0) { - const hasAIResponseAfterLastUser = - hasAIResponseAfterLastUserMessage(history); + const sessionDurationMs = + Date.now() - sessionStats.sessionStartTime.getTime(); - const sessionDurationMs = - Date.now() - sessionStats.sessionStartTime.getTime(); + // Get tool calls count and user messages count + const toolCallsCount = sessionStats.metrics.tools.totalCalls; + const userMessagesCount = countUserMessages(history); - // Get tool calls count and user messages count - const toolCallsCount = sessionStats.metrics.tools.totalCalls; - const userMessagesCount = countUserMessages(history); + // Check if the session meets the minimum requirements: + // Either tool calls > 10 OR user messages > 5 + const meetsMinimumRequirements = + toolCallsCount > MIN_TOOL_CALLS || + userMessagesCount > MIN_USER_MESSAGES; - // Check if the session meets the minimum requirements: - // Either tool calls > 10 OR user messages > 5 - const meetsMinimumRequirements = - toolCallsCount > MIN_TOOL_CALLS || - userMessagesCount > MIN_USER_MESSAGES; + // Check fatigue mechanism (async) + let passedFatigueCheck = false; + try { + passedFatigueCheck = await shouldShowFeedbackBasedOnFatigue(); + } catch (error) { + console.warn('Failed to check feedback fatigue:', error); + } - // Show feedback dialog if: - // 1. Telemetry is enabled (required for feedback submission) - // 2. User feedback is enabled in settings - // 3. There's an AI response after the last user message (real AI conversation) - // 4. Session duration > 10 seconds (meaningful interaction) - // 5. Not already shown for this session - // 6. Random chance (25% probability) - // 7. Meets minimum requirements (tool calls > 10 OR user messages > 5) - if ( - config.getUsageStatisticsEnabled() && // Only show if telemetry is enabled - settings.merged.ui?.enableUserFeedback !== false && // Default to true if not set - hasAIResponseAfterLastUser && - sessionDurationMs > 10000 && // 10 seconds minimum for meaningful interaction - !feedbackShownForSession && - Math.random() < FEEDBACK_SHOW_PROBABILITY && - meetsMinimumRequirements - ) { - timeoutId = setTimeout(() => { + // Show feedback dialog if: + // 1. Telemetry is enabled (required for feedback submission) + // 2. User feedback is enabled in settings + // 3. There's an AI response after the last user message (real AI conversation) + // 4. Session duration > 10 seconds (meaningful interaction) + // 5. Not already shown for this session + // 6. Random chance (25% probability) + // 7. Meets minimum requirements (tool calls > 10 OR user messages > 5) + // 8. Fatigue mechanism allows showing (not shown recently across sessions) + if ( + config.getUsageStatisticsEnabled() && + settings.merged.ui?.enableUserFeedback !== false && + hasAIResponseAfterLastUser && + sessionDurationMs > 10000 && + !feedbackShownForSession && + Math.random() < FEEDBACK_SHOW_PROBABILITY && + meetsMinimumRequirements && + passedFatigueCheck + ) { openFeedbackDialog(); - }, 1000); // Delay to ensure user has time to see the completion - } - } - - return () => { - if (timeoutId) { - clearTimeout(timeoutId); + } } }; + + checkAndShowFeedback().catch((error) => { + console.warn('Error in feedback check:', error); + }); }, [ streamingState, history, From 45236b6ec50ed655b44eef62ad2f68f3cee33402 Mon Sep 17 00:00:00 2001 From: DragonnZhang <731557579@qq.com> Date: Thu, 15 Jan 2026 00:44:14 +0800 Subject: [PATCH 08/10] feat: Integrate UI state management into feedback dialog logic --- packages/cli/src/ui/AppContainer.tsx | 27 ++++++++++--------- .../cli/src/ui/hooks/useFeedbackDialog.ts | 23 +++++++--------- 2 files changed, 23 insertions(+), 27 deletions(-) diff --git a/packages/cli/src/ui/AppContainer.tsx b/packages/cli/src/ui/AppContainer.tsx index 3c6f829c9..6235f7ffc 100644 --- a/packages/cli/src/ui/AppContainer.tsx +++ b/packages/cli/src/ui/AppContainer.tsx @@ -1174,19 +1174,6 @@ export const AppContainer = (props: AppContainerProps) => { const nightly = props.version.includes('nightly'); - const { - isFeedbackDialogOpen, - openFeedbackDialog, - closeFeedbackDialog, - submitFeedback, - } = useFeedbackDialog({ - config, - settings, - streamingState, - history: historyManager.history, - sessionStats, - }); - const dialogsVisible = showWelcomeBackDialog || showWorkspaceMigrationDialog || @@ -1210,6 +1197,20 @@ export const AppContainer = (props: AppContainerProps) => { isApprovalModeDialogOpen || isResumeDialogOpen; + const { + isFeedbackDialogOpen, + openFeedbackDialog, + closeFeedbackDialog, + submitFeedback, + } = useFeedbackDialog({ + config, + settings, + streamingState, + history: historyManager.history, + sessionStats, + dialogsVisible, + }); + const pendingHistoryItems = useMemo( () => [...pendingSlashCommandHistoryItems, ...pendingGeminiHistoryItems], [pendingSlashCommandHistoryItems, pendingGeminiHistoryItems], diff --git a/packages/cli/src/ui/hooks/useFeedbackDialog.ts b/packages/cli/src/ui/hooks/useFeedbackDialog.ts index bfc5e82ca..abb663f09 100644 --- a/packages/cli/src/ui/hooks/useFeedbackDialog.ts +++ b/packages/cli/src/ui/hooks/useFeedbackDialog.ts @@ -122,6 +122,7 @@ export interface UseFeedbackDialogProps { streamingState: StreamingState; history: HistoryItem[]; sessionStats: SessionStatsState; + dialogsVisible: boolean; } export const useFeedbackDialog = ({ @@ -130,14 +131,13 @@ export const useFeedbackDialog = ({ streamingState, history, sessionStats, + dialogsVisible, }: UseFeedbackDialogProps) => { // Feedback dialog state const [isFeedbackDialogOpen, setIsFeedbackDialogOpen] = useState(false); - const [feedbackShownForSession, setFeedbackShownForSession] = useState(false); const openFeedbackDialog = useCallback(() => { setIsFeedbackDialogOpen(true); - setFeedbackShownForSession(true); // Record the timestamp when feedback dialog is shown (fire and forget) saveFeedbackHistory({ @@ -187,9 +187,6 @@ export const useFeedbackDialog = ({ const hasAIResponseAfterLastUser = hasAIResponseAfterLastUserMessage(history); - const sessionDurationMs = - Date.now() - sessionStats.sessionStartTime.getTime(); - // Get tool calls count and user messages count const toolCallsCount = sessionStats.metrics.tools.totalCalls; const userMessagesCount = countUserMessages(history); @@ -209,20 +206,18 @@ export const useFeedbackDialog = ({ } // Show feedback dialog if: - // 1. Telemetry is enabled (required for feedback submission) + // 1. Qwen logger is enabled (required for feedback submission) // 2. User feedback is enabled in settings // 3. There's an AI response after the last user message (real AI conversation) - // 4. Session duration > 10 seconds (meaningful interaction) - // 5. Not already shown for this session - // 6. Random chance (25% probability) - // 7. Meets minimum requirements (tool calls > 10 OR user messages > 5) - // 8. Fatigue mechanism allows showing (not shown recently across sessions) + // 4. No other dialogs are currently visible + // 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) if ( config.getUsageStatisticsEnabled() && settings.merged.ui?.enableUserFeedback !== false && hasAIResponseAfterLastUser && - sessionDurationMs > 10000 && - !feedbackShownForSession && + !dialogsVisible && Math.random() < FEEDBACK_SHOW_PROBABILITY && meetsMinimumRequirements && passedFatigueCheck @@ -240,10 +235,10 @@ export const useFeedbackDialog = ({ history, sessionStats, isFeedbackDialogOpen, - feedbackShownForSession, openFeedbackDialog, settings.merged.ui?.enableUserFeedback, config, + dialogsVisible, ]); return { From e8356c5f9e581da88ff408b2e258b1f5f901f595 Mon Sep 17 00:00:00 2001 From: DragonnZhang <731557579@qq.com> Date: Mon, 19 Jan 2026 13:06:06 +0800 Subject: [PATCH 09/10] feat: Add lastShownTimestamp to settings schema and update feedback dialog logic --- packages/cli/src/config/settingsSchema.ts | 9 + packages/cli/src/i18n/locales/de.js | 1 + packages/cli/src/i18n/locales/en.js | 1 + packages/cli/src/i18n/locales/ru.js | 1 + packages/cli/src/i18n/locales/zh.js | 1 + packages/cli/src/ui/AppContainer.tsx | 1 - packages/cli/src/ui/FeedbackDialog.tsx | 10 +- .../cli/src/ui/hooks/useFeedbackDialog.ts | 199 ++++++------------ packages/core/src/telemetry/loggers.ts | 2 +- .../src/telemetry/qwen-logger/qwen-logger.ts | 2 - packages/core/src/telemetry/types.ts | 6 - 11 files changed, 78 insertions(+), 155 deletions(-) diff --git a/packages/cli/src/config/settingsSchema.ts b/packages/cli/src/config/settingsSchema.ts index c98eb437d..928a9c46d 100644 --- a/packages/cli/src/config/settingsSchema.ts +++ b/packages/cli/src/config/settingsSchema.ts @@ -474,6 +474,15 @@ const SETTINGS_SCHEMA = { }, }, }, + lastShownTimestamp: { + type: 'number', + label: 'Feedback Last Shown Timestamp', + category: 'UI', + requiresRestart: false, + default: 0, + description: 'The last time the feedback dialog was shown.', + showInDialog: false, + }, }, }, diff --git a/packages/cli/src/i18n/locales/de.js b/packages/cli/src/i18n/locales/de.js index ec6d6a8e6..d358811cf 100644 --- a/packages/cli/src/i18n/locales/de.js +++ b/packages/cli/src/i18n/locales/de.js @@ -295,6 +295,7 @@ export default { Bad: 'Schlecht', Good: 'Gut', 'Not Sure Yet': 'Noch nicht sicher', + 'Any other key': 'Beliebige andere Taste', 'Disable Loading Phrases': 'Ladesprüche deaktivieren', 'Screen Reader Mode': 'Bildschirmleser-Modus', 'IDE Mode': 'IDE-Modus', diff --git a/packages/cli/src/i18n/locales/en.js b/packages/cli/src/i18n/locales/en.js index 934e97868..e3287731d 100644 --- a/packages/cli/src/i18n/locales/en.js +++ b/packages/cli/src/i18n/locales/en.js @@ -292,6 +292,7 @@ export default { Bad: 'Bad', Good: 'Good', 'Not Sure Yet': 'Not Sure Yet', + 'Any other key': 'Any other key', 'Disable Loading Phrases': 'Disable Loading Phrases', 'Screen Reader Mode': 'Screen Reader Mode', 'IDE Mode': 'IDE Mode', diff --git a/packages/cli/src/i18n/locales/ru.js b/packages/cli/src/i18n/locales/ru.js index 857aa1821..a91cd0d44 100644 --- a/packages/cli/src/i18n/locales/ru.js +++ b/packages/cli/src/i18n/locales/ru.js @@ -295,6 +295,7 @@ export default { Bad: 'Плохо', Good: 'Хорошо', 'Not Sure Yet': 'Пока не уверен', + 'Any other key': 'Любая другая клавиша', 'Disable Loading Phrases': 'Отключить фразы при загрузке', 'Screen Reader Mode': 'Режим программы чтения с экрана', 'IDE Mode': 'Режим IDE', diff --git a/packages/cli/src/i18n/locales/zh.js b/packages/cli/src/i18n/locales/zh.js index e659da0a3..75664c547 100644 --- a/packages/cli/src/i18n/locales/zh.js +++ b/packages/cli/src/i18n/locales/zh.js @@ -282,6 +282,7 @@ export default { Bad: '不满意', Good: '满意', 'Not Sure Yet': '暂不评价', + 'Any other key': '任意其他键', 'Disable Loading Phrases': '禁用加载短语', 'Screen Reader Mode': '屏幕阅读器模式', 'IDE Mode': 'IDE 模式', diff --git a/packages/cli/src/ui/AppContainer.tsx b/packages/cli/src/ui/AppContainer.tsx index 6235f7ffc..6d53994f0 100644 --- a/packages/cli/src/ui/AppContainer.tsx +++ b/packages/cli/src/ui/AppContainer.tsx @@ -1208,7 +1208,6 @@ export const AppContainer = (props: AppContainerProps) => { streamingState, history: historyManager.history, sessionStats, - dialogsVisible, }); const pendingHistoryItems = useMemo( diff --git a/packages/cli/src/ui/FeedbackDialog.tsx b/packages/cli/src/ui/FeedbackDialog.tsx index 3984664d6..7791dfb88 100644 --- a/packages/cli/src/ui/FeedbackDialog.tsx +++ b/packages/cli/src/ui/FeedbackDialog.tsx @@ -14,10 +14,10 @@ const FEEDBACK_OPTIONS = { const FEEDBACK_OPTION_KEYS = { [FEEDBACK_OPTIONS.GOOD]: '1', [FEEDBACK_OPTIONS.BAD]: '2', - [FEEDBACK_OPTIONS.NOT_SURE]: '3', + [FEEDBACK_OPTIONS.NOT_SURE]: 'any', } as const; -export const FEEDBACK_DIALOG_KEYS = ['1', '2', '3'] as const; +export const FEEDBACK_DIALOG_KEYS = ['1', '2'] as const; export const FeedbackDialog: React.FC = () => { const uiState = useUIState(); @@ -29,7 +29,7 @@ export const FeedbackDialog: React.FC = () => { 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]) { + } else { uiActions.submitFeedback(FEEDBACK_OPTIONS.NOT_SURE); } @@ -53,9 +53,7 @@ export const FeedbackDialog: React.FC = () => { {FEEDBACK_OPTION_KEYS[FEEDBACK_OPTIONS.BAD]}: {t('Bad')} - - {FEEDBACK_OPTION_KEYS[FEEDBACK_OPTIONS.NOT_SURE]}:{' '} - + {t('Any other key')}: {t('Not Sure Yet')} diff --git a/packages/cli/src/ui/hooks/useFeedbackDialog.ts b/packages/cli/src/ui/hooks/useFeedbackDialog.ts index abb663f09..b5b50103c 100644 --- a/packages/cli/src/ui/hooks/useFeedbackDialog.ts +++ b/packages/cli/src/ui/hooks/useFeedbackDialog.ts @@ -1,17 +1,20 @@ import { useState, useCallback, useEffect } from 'react'; -import * as fs from 'node:fs/promises'; -import * as path from 'node:path'; +import * as fs from 'node:fs'; import { type Config, logUserFeedback, UserFeedbackEvent, type UserFeedbackRating, - Storage, isNodeError, } from '@qwen-code/qwen-code-core'; import { StreamingState, MessageType, type HistoryItem } from '../types.js'; -import type { LoadedSettings } from '../../config/settings.js'; +import { + SettingScope, + type LoadedSettings, + USER_SETTINGS_PATH, +} from '../../config/settings.js'; import type { SessionStatsState } from '../contexts/SessionContext.js'; +import stripJsonComments from 'strip-json-comments'; const FEEDBACK_SHOW_PROBABILITY = 0.25; // 25% probability of showing feedback dialog const MIN_TOOL_CALLS = 10; // Minimum tool calls to show feedback dialog @@ -19,110 +22,68 @@ const MIN_USER_MESSAGES = 5; // Minimum user messages to show feedback dialog // Fatigue mechanism constants const FEEDBACK_COOLDOWN_HOURS = 24; // Hours to wait before showing feedback dialog again -const FEEDBACK_HISTORY_FILENAME = 'feedback-history.json'; /** - * Check if there's an AI response after the last user message in the conversation history + * Check if the last message in the conversation history is an AI response */ -const hasAIResponseAfterLastUserMessage = (history: HistoryItem[]): boolean => { - // Find the last user message - let lastUserMessageIndex = -1; - for (let i = history.length - 1; i >= 0; i--) { - if (history[i].type === MessageType.USER) { - lastUserMessageIndex = i; - break; - } - } - - // Check if there's any AI response (GEMINI message) after the last user message - if (lastUserMessageIndex !== -1) { - for (let i = lastUserMessageIndex + 1; i < history.length; i++) { - if (history[i].type === MessageType.GEMINI) { - return true; - } - } - } - - return false; -}; +const lastMessageIsAIResponse = (history: HistoryItem[]): boolean => + history.length > 0 && history[history.length - 1].type === MessageType.GEMINI; /** - * Count the number of user messages in the conversation history + * Read lastShownTimestamp directly from the user settings file */ -const countUserMessages = (history: HistoryItem[]): number => - history.filter((item) => item.type === MessageType.USER).length; - -/** - * Interface for feedback history storage - */ -interface FeedbackHistory { - lastShownTimestamp: number; -} - -/** - * Get the feedback history file path using global Storage - */ -function getFeedbackHistoryPath(): string { - const globalQwenDir = Storage.getGlobalQwenDir(); - return path.join(globalQwenDir, FEEDBACK_HISTORY_FILENAME); -} - -/** - * Get the last feedback dialog show time from file storage - */ -const getFeedbackHistory = async (): Promise => { +const getLastShownTimestampFromFile = (): number => { try { - const filePath = getFeedbackHistoryPath(); - const content = await fs.readFile(filePath, 'utf-8'); - return JSON.parse(content) as FeedbackHistory; - } catch (error) { - if (isNodeError(error) && error.code === 'ENOENT') { - // File doesn't exist yet, which is normal for first time - return null; + if (fs.existsSync(USER_SETTINGS_PATH)) { + const content = fs.readFileSync(USER_SETTINGS_PATH, 'utf-8'); + const settings = JSON.parse(stripJsonComments(content)); + return settings?.ui?.lastShownTimestamp ?? 0; } - console.warn('Failed to read feedback history from file:', error); - return null; - } -}; - -/** - * Save feedback history to file storage - */ -const saveFeedbackHistory = async (history: FeedbackHistory): Promise => { - try { - const filePath = getFeedbackHistoryPath(); - - // Ensure the directory exists - await fs.mkdir(path.dirname(filePath), { recursive: true }); - - // Write the history file - await fs.writeFile(filePath, JSON.stringify(history, null, 2), 'utf-8'); } catch (error) { - console.warn('Failed to save feedback history to file:', error); + if (isNodeError(error) && error.code !== 'ENOENT') { + console.warn( + 'Failed to read lastShownTimestamp from settings file:', + error, + ); + } } + return 0; }; /** * Check if we should show the feedback dialog based on fatigue mechanism */ -const shouldShowFeedbackBasedOnFatigue = async (): Promise => { - const history = await getFeedbackHistory(); - if (!history) return true; // No history, allow showing +const shouldShowFeedbackBasedOnFatigue = (): boolean => { + const lastShownTimestamp = getLastShownTimestampFromFile(); const now = Date.now(); - const timeSinceLastShown = now - history.lastShownTimestamp; + const timeSinceLastShown = now - lastShownTimestamp; const cooldownMs = FEEDBACK_COOLDOWN_HOURS * 60 * 60 * 1000; return timeSinceLastShown >= cooldownMs; }; +/** + * Check if the session meets the minimum requirements for showing feedback + * Either tool calls > 10 OR user messages > 5 + */ +const meetsMinimumSessionRequirements = ( + sessionStats: SessionStatsState, +): boolean => { + const toolCallsCount = sessionStats.metrics.tools.totalCalls; + const userMessagesCount = sessionStats.promptCount; + + return ( + toolCallsCount > MIN_TOOL_CALLS || userMessagesCount > MIN_USER_MESSAGES + ); +}; + export interface UseFeedbackDialogProps { config: Config; settings: LoadedSettings; streamingState: StreamingState; history: HistoryItem[]; sessionStats: SessionStatsState; - dialogsVisible: boolean; } export const useFeedbackDialog = ({ @@ -131,7 +92,6 @@ export const useFeedbackDialog = ({ streamingState, history, sessionStats, - dialogsVisible, }: UseFeedbackDialogProps) => { // Feedback dialog state const [isFeedbackDialogOpen, setIsFeedbackDialogOpen] = useState(false); @@ -140,10 +100,8 @@ export const useFeedbackDialog = ({ setIsFeedbackDialogOpen(true); // Record the timestamp when feedback dialog is shown (fire and forget) - saveFeedbackHistory({ - lastShownTimestamp: Date.now(), - }); - }, []); + settings.setValue(SettingScope.User, 'ui.lastShownTimestamp', Date.now()); + }, [settings]); const closeFeedbackDialog = useCallback( () => setIsFeedbackDialogOpen(false), @@ -152,25 +110,10 @@ export const useFeedbackDialog = ({ const submitFeedback = useCallback( (rating: number) => { - // Calculate session duration and turn count - const sessionDurationMs = - Date.now() - sessionStats.sessionStartTime.getTime(); - let lastUserMessageIndex = -1; - for (let i = history.length - 1; i >= 0; i--) { - if (history[i].type === MessageType.USER) { - lastUserMessageIndex = i; - break; - } - } - const turnCount = - lastUserMessageIndex === -1 ? 0 : history.length - lastUserMessageIndex; - // Create and log the feedback event const feedbackEvent = new UserFeedbackEvent( sessionStats.sessionId, rating as UserFeedbackRating, - sessionDurationMs, - turnCount, config.getModel(), config.getApprovalMode(), ); @@ -178,58 +121,37 @@ export const useFeedbackDialog = ({ logUserFeedback(config, feedbackEvent); closeFeedbackDialog(); }, - [config, sessionStats, history, closeFeedbackDialog], + [config, sessionStats, closeFeedbackDialog], ); useEffect(() => { - const checkAndShowFeedback = async () => { + const checkAndShowFeedback = () => { if (streamingState === StreamingState.Idle && history.length > 0) { - const hasAIResponseAfterLastUser = - hasAIResponseAfterLastUserMessage(history); - - // Get tool calls count and user messages count - const toolCallsCount = sessionStats.metrics.tools.totalCalls; - const userMessagesCount = countUserMessages(history); - - // Check if the session meets the minimum requirements: - // Either tool calls > 10 OR user messages > 5 - const meetsMinimumRequirements = - toolCallsCount > MIN_TOOL_CALLS || - userMessagesCount > MIN_USER_MESSAGES; - - // Check fatigue mechanism (async) - let passedFatigueCheck = false; - try { - passedFatigueCheck = await shouldShowFeedbackBasedOnFatigue(); - } catch (error) { - console.warn('Failed to check feedback fatigue:', error); - } - // Show feedback dialog if: // 1. Qwen logger is enabled (required for feedback submission) // 2. User feedback is enabled in settings - // 3. There's an AI response after the last user message (real AI conversation) - // 4. No other dialogs are currently visible - // 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) + // 3. The last message is an AI response + // 4. Random chance (25% probability) + // 5. Meets minimum requirements (tool calls > 10 OR user messages > 5) + // 6. Fatigue mechanism allows showing (not shown recently across sessions) if ( - config.getUsageStatisticsEnabled() && - settings.merged.ui?.enableUserFeedback !== false && - hasAIResponseAfterLastUser && - !dialogsVisible && - Math.random() < FEEDBACK_SHOW_PROBABILITY && - meetsMinimumRequirements && - passedFatigueCheck + !config.getUsageStatisticsEnabled() || + settings.merged.ui?.enableUserFeedback === false || + !lastMessageIsAIResponse(history) || + Math.random() > FEEDBACK_SHOW_PROBABILITY || + !meetsMinimumSessionRequirements(sessionStats) ) { + return; + } + + // Check fatigue mechanism (synchronous) + if (shouldShowFeedbackBasedOnFatigue()) { openFeedbackDialog(); } } }; - checkAndShowFeedback().catch((error) => { - console.warn('Error in feedback check:', error); - }); + checkAndShowFeedback(); }, [ streamingState, history, @@ -238,7 +160,6 @@ export const useFeedbackDialog = ({ openFeedbackDialog, settings.merged.ui?.enableUserFeedback, config, - dialogsVisible, ]); return { diff --git a/packages/core/src/telemetry/loggers.ts b/packages/core/src/telemetry/loggers.ts index 446acfda0..1acf0e57c 100644 --- a/packages/core/src/telemetry/loggers.ts +++ b/packages/core/src/telemetry/loggers.ts @@ -913,7 +913,7 @@ export function logUserFeedback( const logger = logs.getLogger(SERVICE_NAME); const logRecord: LogRecord = { - body: `User feedback: Rating ${event.rating} for session ${event.session_id}. Turn count: ${event.turn_count}. Duration: ${event.session_duration_ms}ms.`, + body: `User feedback: Rating ${event.rating} for session ${event.session_id}.`, attributes, }; logger.emit(logRecord); diff --git a/packages/core/src/telemetry/qwen-logger/qwen-logger.ts b/packages/core/src/telemetry/qwen-logger/qwen-logger.ts index 76a63231c..c33287cb2 100644 --- a/packages/core/src/telemetry/qwen-logger/qwen-logger.ts +++ b/packages/core/src/telemetry/qwen-logger/qwen-logger.ts @@ -848,8 +848,6 @@ export class QwenLogger { properties: { session_id: event.session_id, rating: event.rating, - session_duration_ms: event.session_duration_ms, - turn_count: event.turn_count, model: event.model, approval_mode: event.approval_mode, prompt_id: event.prompt_id || '', diff --git a/packages/core/src/telemetry/types.ts b/packages/core/src/telemetry/types.ts index 528fd0200..5b7793f23 100644 --- a/packages/core/src/telemetry/types.ts +++ b/packages/core/src/telemetry/types.ts @@ -768,8 +768,6 @@ export class UserFeedbackEvent implements BaseTelemetryEvent { 'event.timestamp': string; session_id: string; rating: UserFeedbackRating; - session_duration_ms: number; - turn_count: number; model: string; approval_mode: string; prompt_id?: string; @@ -777,8 +775,6 @@ export class UserFeedbackEvent implements BaseTelemetryEvent { constructor( session_id: string, rating: UserFeedbackRating, - session_duration_ms: number, - turn_count: number, model: string, approval_mode: string, prompt_id?: string, @@ -787,8 +783,6 @@ export class UserFeedbackEvent implements BaseTelemetryEvent { this['event.timestamp'] = new Date().toISOString(); this.session_id = session_id; this.rating = rating; - this.session_duration_ms = session_duration_ms; - this.turn_count = turn_count; this.model = model; this.approval_mode = approval_mode; this.prompt_id = prompt_id; From f99295462d5fe63c57426fcece4230a4b5dff763 Mon Sep 17 00:00:00 2001 From: DragonnZhang <731557579@qq.com> Date: Mon, 19 Jan 2026 16:19:35 +0800 Subject: [PATCH 10/10] feat: Rename lastShownTimestamp to feedbackLastShownTimestamp and check QWEN_OAUTH for feedback dialog showing --- packages/cli/src/config/settingsSchema.ts | 2 +- .../cli/src/ui/components/InputPrompt.tsx | 2 +- .../cli/src/ui/hooks/useFeedbackDialog.ts | 33 +++++++++++-------- 3 files changed, 22 insertions(+), 15 deletions(-) diff --git a/packages/cli/src/config/settingsSchema.ts b/packages/cli/src/config/settingsSchema.ts index 928a9c46d..ddb52c212 100644 --- a/packages/cli/src/config/settingsSchema.ts +++ b/packages/cli/src/config/settingsSchema.ts @@ -474,7 +474,7 @@ const SETTINGS_SCHEMA = { }, }, }, - lastShownTimestamp: { + feedbackLastShownTimestamp: { type: 'number', label: 'Feedback Last Shown Timestamp', category: 'UI', diff --git a/packages/cli/src/ui/components/InputPrompt.tsx b/packages/cli/src/ui/components/InputPrompt.tsx index 1f7ed099a..b640bcd61 100644 --- a/packages/cli/src/ui/components/InputPrompt.tsx +++ b/packages/cli/src/ui/components/InputPrompt.tsx @@ -329,7 +329,7 @@ export const InputPrompt: React.FC = ({ return; } - // Intercept feedback dialog option keys (1, 2, 3) when dialog is open + // Intercept feedback dialog option keys (1, 2) when dialog is open if ( uiState.isFeedbackDialogOpen && (FEEDBACK_DIALOG_KEYS as readonly string[]).includes(key.name) diff --git a/packages/cli/src/ui/hooks/useFeedbackDialog.ts b/packages/cli/src/ui/hooks/useFeedbackDialog.ts index b5b50103c..18865b1f0 100644 --- a/packages/cli/src/ui/hooks/useFeedbackDialog.ts +++ b/packages/cli/src/ui/hooks/useFeedbackDialog.ts @@ -6,6 +6,7 @@ import { UserFeedbackEvent, type UserFeedbackRating, isNodeError, + AuthType, } from '@qwen-code/qwen-code-core'; import { StreamingState, MessageType, type HistoryItem } from '../types.js'; import { @@ -30,19 +31,19 @@ const lastMessageIsAIResponse = (history: HistoryItem[]): boolean => history.length > 0 && history[history.length - 1].type === MessageType.GEMINI; /** - * Read lastShownTimestamp directly from the user settings file + * Read feedbackLastShownTimestamp directly from the user settings file */ -const getLastShownTimestampFromFile = (): number => { +const getFeedbackLastShownTimestampFromFile = (): number => { try { if (fs.existsSync(USER_SETTINGS_PATH)) { const content = fs.readFileSync(USER_SETTINGS_PATH, 'utf-8'); const settings = JSON.parse(stripJsonComments(content)); - return settings?.ui?.lastShownTimestamp ?? 0; + return settings?.ui?.feedbackLastShownTimestamp ?? 0; } } catch (error) { if (isNodeError(error) && error.code !== 'ENOENT') { console.warn( - 'Failed to read lastShownTimestamp from settings file:', + 'Failed to read feedbackLastShownTimestamp from settings file:', error, ); } @@ -54,10 +55,10 @@ const getLastShownTimestampFromFile = (): number => { * Check if we should show the feedback dialog based on fatigue mechanism */ const shouldShowFeedbackBasedOnFatigue = (): boolean => { - const lastShownTimestamp = getLastShownTimestampFromFile(); + const feedbackLastShownTimestamp = getFeedbackLastShownTimestampFromFile(); const now = Date.now(); - const timeSinceLastShown = now - lastShownTimestamp; + const timeSinceLastShown = now - feedbackLastShownTimestamp; const cooldownMs = FEEDBACK_COOLDOWN_HOURS * 60 * 60 * 1000; return timeSinceLastShown >= cooldownMs; @@ -100,7 +101,11 @@ export const useFeedbackDialog = ({ setIsFeedbackDialogOpen(true); // Record the timestamp when feedback dialog is shown (fire and forget) - settings.setValue(SettingScope.User, 'ui.lastShownTimestamp', Date.now()); + settings.setValue( + SettingScope.User, + 'ui.feedbackLastShownTimestamp', + Date.now(), + ); }, [settings]); const closeFeedbackDialog = useCallback( @@ -128,13 +133,15 @@ export const useFeedbackDialog = ({ const checkAndShowFeedback = () => { if (streamingState === StreamingState.Idle && history.length > 0) { // Show feedback dialog if: - // 1. Qwen logger is enabled (required for feedback submission) - // 2. User feedback is enabled in settings - // 3. The last message is an AI response - // 4. Random chance (25% probability) - // 5. Meets minimum requirements (tool calls > 10 OR user messages > 5) - // 6. Fatigue mechanism allows showing (not shown recently across sessions) + // 1. User is authenticated via QWEN_OAUTH + // 2. Qwen logger is enabled (required for feedback submission) + // 3. User feedback is enabled in settings + // 4. The last message is an AI response + // 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) if ( + config.getAuthType() !== AuthType.QWEN_OAUTH || !config.getUsageStatisticsEnabled() || settings.merged.ui?.enableUserFeedback === false || !lastMessageIsAIResponse(history) ||