diff --git a/packages/cli/src/config/settingsSchema.ts b/packages/cli/src/config/settingsSchema.ts
index cae4d2c66..3b0356ba8 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',
@@ -464,6 +474,15 @@ const SETTINGS_SCHEMA = {
},
},
},
+ feedbackLastShownTimestamp: {
+ 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 fa4221854..d358811cf 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 Qwen doing this session? (optional)':
+ 'Wie macht sich Qwen in dieser Sitzung? (optional)',
+ 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 51461f4cb..e3287731d 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 Qwen doing this session? (optional)':
+ 'How is Qwen doing this session? (optional)',
+ 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 82f2436ef..a91cd0d44 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 Qwen doing this session? (optional)':
+ 'Как дела у Qwen в этой сессии? (необязательно)',
+ 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 199ea572b..466b163e3 100644
--- a/packages/cli/src/i18n/locales/zh.js
+++ b/packages/cli/src/i18n/locales/zh.js
@@ -277,6 +277,12 @@ export default {
'Show Citations': '显示引用',
'Custom Witty Phrases': '自定义诙谐短语',
'Enable Welcome Back': '启用欢迎回来',
+ 'Enable User Feedback': '启用用户反馈',
+ 'How is Qwen doing this session? (optional)': 'Qwen 这次表现如何?(可选)',
+ 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 685d818ca..909d2beec 100644
--- a/packages/cli/src/ui/AppContainer.tsx
+++ b/packages/cli/src/ui/AppContainer.tsx
@@ -45,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';
@@ -1195,6 +1196,19 @@ export const AppContainer = (props: AppContainerProps) => {
isApprovalModeDialogOpen ||
isResumeDialogOpen;
+ const {
+ isFeedbackDialogOpen,
+ openFeedbackDialog,
+ closeFeedbackDialog,
+ submitFeedback,
+ } = useFeedbackDialog({
+ config,
+ settings,
+ streamingState,
+ history: historyManager.history,
+ sessionStats,
+ });
+
const pendingHistoryItems = useMemo(
() => [...pendingSlashCommandHistoryItems, ...pendingGeminiHistoryItems],
[pendingSlashCommandHistoryItems, pendingGeminiHistoryItems],
@@ -1291,6 +1305,8 @@ export const AppContainer = (props: AppContainerProps) => {
// Subagent dialogs
isSubagentCreateDialogOpen,
isAgentsManagerDialogOpen,
+ // Feedback dialog
+ isFeedbackDialogOpen,
}),
[
isThemeDialogOpen,
@@ -1381,6 +1397,8 @@ export const AppContainer = (props: AppContainerProps) => {
// Subagent dialogs
isSubagentCreateDialogOpen,
isAgentsManagerDialogOpen,
+ // Feedback dialog
+ isFeedbackDialogOpen,
],
);
@@ -1421,6 +1439,10 @@ export const AppContainer = (props: AppContainerProps) => {
openResumeDialog,
closeResumeDialog,
handleResume,
+ // Feedback dialog
+ openFeedbackDialog,
+ closeFeedbackDialog,
+ submitFeedback,
}),
[
handleThemeSelect,
@@ -1456,6 +1478,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..7791dfb88
--- /dev/null
+++ b/packages/cli/src/ui/FeedbackDialog.tsx
@@ -0,0 +1,61 @@
+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';
+
+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]: 'any',
+} as const;
+
+export const FEEDBACK_DIALOG_KEYS = ['1', '2'] as const;
+
+export const FeedbackDialog: React.FC = () => {
+ const uiState = useUIState();
+ const uiActions = useUIActions();
+
+ 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]) {
+ uiActions.submitFeedback(FEEDBACK_OPTIONS.BAD);
+ } else {
+ uiActions.submitFeedback(FEEDBACK_OPTIONS.NOT_SURE);
+ }
+
+ uiActions.closeFeedbackDialog();
+ },
+ { isActive: uiState.isFeedbackDialogOpen },
+ );
+
+ return (
+
+
+ ●
+ {t('How is Qwen doing this session? (optional)')}
+
+
+
+ {FEEDBACK_OPTION_KEYS[FEEDBACK_OPTIONS.GOOD]}:{' '}
+
+ {t('Good')}
+
+ {FEEDBACK_OPTION_KEYS[FEEDBACK_OPTIONS.BAD]}:
+ {t('Bad')}
+
+ {t('Any other key')}:
+ {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 && (
({
+ 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 1370ecdc7..8b59d7a05 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);
@@ -328,6 +331,14 @@ 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;
+ }
+
// Reset ESC count and hide prompt on any non-ESC key
if (key.name !== 'escape') {
if (escPressCount > 0 || showEscapePrompt) {
@@ -672,6 +683,7 @@ export const InputPrompt: React.FC = ({
recentPasteTime,
commandSearchActive,
commandSearchCompletion,
+ uiState,
],
);
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/cli/src/ui/hooks/useFeedbackDialog.ts b/packages/cli/src/ui/hooks/useFeedbackDialog.ts
new file mode 100644
index 000000000..18865b1f0
--- /dev/null
+++ b/packages/cli/src/ui/hooks/useFeedbackDialog.ts
@@ -0,0 +1,178 @@
+import { useState, useCallback, useEffect } from 'react';
+import * as fs from 'node:fs';
+import {
+ type Config,
+ logUserFeedback,
+ UserFeedbackEvent,
+ type UserFeedbackRating,
+ isNodeError,
+ AuthType,
+} from '@qwen-code/qwen-code-core';
+import { StreamingState, MessageType, type HistoryItem } from '../types.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
+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
+
+/**
+ * Check if the last message in the conversation history is an AI response
+ */
+const lastMessageIsAIResponse = (history: HistoryItem[]): boolean =>
+ history.length > 0 && history[history.length - 1].type === MessageType.GEMINI;
+
+/**
+ * Read feedbackLastShownTimestamp directly from the user settings file
+ */
+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?.feedbackLastShownTimestamp ?? 0;
+ }
+ } catch (error) {
+ if (isNodeError(error) && error.code !== 'ENOENT') {
+ console.warn(
+ 'Failed to read feedbackLastShownTimestamp from settings file:',
+ error,
+ );
+ }
+ }
+ return 0;
+};
+
+/**
+ * Check if we should show the feedback dialog based on fatigue mechanism
+ */
+const shouldShowFeedbackBasedOnFatigue = (): boolean => {
+ const feedbackLastShownTimestamp = getFeedbackLastShownTimestampFromFile();
+
+ const now = Date.now();
+ const timeSinceLastShown = now - feedbackLastShownTimestamp;
+ 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;
+}
+
+export const useFeedbackDialog = ({
+ config,
+ settings,
+ streamingState,
+ history,
+ sessionStats,
+}: UseFeedbackDialogProps) => {
+ // Feedback dialog state
+ const [isFeedbackDialogOpen, setIsFeedbackDialogOpen] = 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 submitFeedback = useCallback(
+ (rating: number) => {
+ // Create and log the feedback event
+ const feedbackEvent = new UserFeedbackEvent(
+ sessionStats.sessionId,
+ rating as UserFeedbackRating,
+ config.getModel(),
+ config.getApprovalMode(),
+ );
+
+ logUserFeedback(config, feedbackEvent);
+ closeFeedbackDialog();
+ },
+ [config, sessionStats, closeFeedbackDialog],
+ );
+
+ useEffect(() => {
+ const checkAndShowFeedback = () => {
+ if (streamingState === StreamingState.Idle && history.length > 0) {
+ // Show feedback dialog if:
+ // 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) ||
+ Math.random() > FEEDBACK_SHOW_PROBABILITY ||
+ !meetsMinimumSessionRequirements(sessionStats)
+ ) {
+ return;
+ }
+
+ // Check fatigue mechanism (synchronous)
+ if (shouldShowFeedbackBasedOnFatigue()) {
+ openFeedbackDialog();
+ }
+ }
+ };
+
+ checkAndShowFeedback();
+ }, [
+ streamingState,
+ history,
+ sessionStats,
+ isFeedbackDialogOpen,
+ openFeedbackDialog,
+ settings.merged.ui?.enableUserFeedback,
+ config,
+ ]);
+
+ return {
+ isFeedbackDialogOpen,
+ openFeedbackDialog,
+ closeFeedbackDialog,
+ submitFeedback,
+ };
+};
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..1acf0e57c 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}.`,
+ 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..c33287cb2 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,21 @@ 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,
+ 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..5b7793f23 100644
--- a/packages/core/src/telemetry/types.ts
+++ b/packages/core/src/telemetry/types.ts
@@ -757,6 +757,38 @@ 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;
+ model: string;
+ approval_mode: string;
+ prompt_id?: string;
+
+ constructor(
+ session_id: string,
+ rating: UserFeedbackRating,
+ 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.model = model;
+ this.approval_mode = approval_mode;
+ this.prompt_id = prompt_id;
+ }
+}
+
export type TelemetryEvent =
| StartSessionEvent
| EndSessionEvent
@@ -786,7 +818,8 @@ export type TelemetryEvent =
| ToolOutputTruncatedEvent
| ModelSlashCommandEvent
| AuthEvent
- | SkillLaunchEvent;
+ | SkillLaunchEvent
+ | UserFeedbackEvent;
export class ExtensionDisableEvent implements BaseTelemetryEvent {
'event.name': 'extension_disable';