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';