mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-05-05 23:42:03 +00:00
Merge branch 'main' into fix/shift-tab-windows-powershell
This commit is contained in:
commit
86ba86e297
352 changed files with 38309 additions and 6463 deletions
|
|
@ -434,7 +434,7 @@ export const AppContainer = (props: AppContainerProps) => {
|
|||
// Check for enforced auth type mismatch
|
||||
useEffect(() => {
|
||||
// Check for initialization error first
|
||||
const currentAuthType = config.modelsConfig.getCurrentAuthType();
|
||||
const currentAuthType = config.getModelsConfig().getCurrentAuthType();
|
||||
|
||||
if (
|
||||
settings.merged.security?.auth?.enforcedType &&
|
||||
|
|
@ -623,7 +623,7 @@ export const AppContainer = (props: AppContainerProps) => {
|
|||
try {
|
||||
const { memoryContent, fileCount } = await loadHierarchicalGeminiMemory(
|
||||
process.cwd(),
|
||||
settings.merged.context?.loadMemoryFromIncludeDirectories
|
||||
settings.merged.context?.loadFromIncludeDirectories
|
||||
? config.getWorkspaceContext().getDirectories()
|
||||
: [],
|
||||
config.getDebugMode(),
|
||||
|
|
@ -1350,6 +1350,7 @@ export const AppContainer = (props: AppContainerProps) => {
|
|||
isFeedbackDialogOpen,
|
||||
openFeedbackDialog,
|
||||
closeFeedbackDialog,
|
||||
temporaryCloseFeedbackDialog,
|
||||
submitFeedback,
|
||||
} = useFeedbackDialog({
|
||||
config,
|
||||
|
|
@ -1597,6 +1598,7 @@ export const AppContainer = (props: AppContainerProps) => {
|
|||
// Feedback dialog
|
||||
openFeedbackDialog,
|
||||
closeFeedbackDialog,
|
||||
temporaryCloseFeedbackDialog,
|
||||
submitFeedback,
|
||||
}),
|
||||
[
|
||||
|
|
@ -1637,6 +1639,7 @@ export const AppContainer = (props: AppContainerProps) => {
|
|||
// Feedback dialog
|
||||
openFeedbackDialog,
|
||||
closeFeedbackDialog,
|
||||
temporaryCloseFeedbackDialog,
|
||||
submitFeedback,
|
||||
],
|
||||
);
|
||||
|
|
|
|||
|
|
@ -5,19 +5,21 @@ import { useUIActions } from './contexts/UIActionsContext.js';
|
|||
import { useUIState } from './contexts/UIStateContext.js';
|
||||
import { useKeypress } from './hooks/useKeypress.js';
|
||||
|
||||
const FEEDBACK_OPTIONS = {
|
||||
export const FEEDBACK_OPTIONS = {
|
||||
GOOD: 1,
|
||||
BAD: 2,
|
||||
NOT_SURE: 3,
|
||||
FINE: 3,
|
||||
DISMISS: 0,
|
||||
} as const;
|
||||
|
||||
const FEEDBACK_OPTION_KEYS = {
|
||||
[FEEDBACK_OPTIONS.GOOD]: '1',
|
||||
[FEEDBACK_OPTIONS.BAD]: '2',
|
||||
[FEEDBACK_OPTIONS.NOT_SURE]: 'any',
|
||||
[FEEDBACK_OPTIONS.FINE]: '3',
|
||||
[FEEDBACK_OPTIONS.DISMISS]: '0',
|
||||
} as const;
|
||||
|
||||
export const FEEDBACK_DIALOG_KEYS = ['1', '2'] as const;
|
||||
export const FEEDBACK_DIALOG_KEYS = ['1', '2', '3', '0'] as const;
|
||||
|
||||
export const FeedbackDialog: React.FC = () => {
|
||||
const uiState = useUIState();
|
||||
|
|
@ -25,15 +27,19 @@ export const FeedbackDialog: React.FC = () => {
|
|||
|
||||
useKeypress(
|
||||
(key) => {
|
||||
if (key.name === FEEDBACK_OPTION_KEYS[FEEDBACK_OPTIONS.GOOD]) {
|
||||
uiActions.submitFeedback(FEEDBACK_OPTIONS.GOOD);
|
||||
} else if (key.name === FEEDBACK_OPTION_KEYS[FEEDBACK_OPTIONS.BAD]) {
|
||||
// Handle keys 0-3: permanent close with feedback/dismiss
|
||||
if (key.name === FEEDBACK_OPTION_KEYS[FEEDBACK_OPTIONS.BAD]) {
|
||||
uiActions.submitFeedback(FEEDBACK_OPTIONS.BAD);
|
||||
} else if (key.name === FEEDBACK_OPTION_KEYS[FEEDBACK_OPTIONS.FINE]) {
|
||||
uiActions.submitFeedback(FEEDBACK_OPTIONS.FINE);
|
||||
} else if (key.name === FEEDBACK_OPTION_KEYS[FEEDBACK_OPTIONS.GOOD]) {
|
||||
uiActions.submitFeedback(FEEDBACK_OPTIONS.GOOD);
|
||||
} else if (key.name === FEEDBACK_OPTION_KEYS[FEEDBACK_OPTIONS.DISMISS]) {
|
||||
uiActions.submitFeedback(FEEDBACK_OPTIONS.DISMISS);
|
||||
} else {
|
||||
uiActions.submitFeedback(FEEDBACK_OPTIONS.NOT_SURE);
|
||||
// Handle other keys: temporary close
|
||||
uiActions.temporaryCloseFeedbackDialog();
|
||||
}
|
||||
|
||||
uiActions.closeFeedbackDialog();
|
||||
},
|
||||
{ isActive: uiState.isFeedbackDialogOpen },
|
||||
);
|
||||
|
|
@ -53,8 +59,16 @@ export const FeedbackDialog: React.FC = () => {
|
|||
<Text color="cyan">{FEEDBACK_OPTION_KEYS[FEEDBACK_OPTIONS.BAD]}: </Text>
|
||||
<Text>{t('Bad')}</Text>
|
||||
<Text> </Text>
|
||||
<Text color="cyan">{t('Any other key')}: </Text>
|
||||
<Text>{t('Not Sure Yet')}</Text>
|
||||
<Text color="cyan">
|
||||
{FEEDBACK_OPTION_KEYS[FEEDBACK_OPTIONS.FINE]}:{' '}
|
||||
</Text>
|
||||
<Text>{t('Fine')}</Text>
|
||||
<Text> </Text>
|
||||
<Text color="cyan">
|
||||
{FEEDBACK_OPTION_KEYS[FEEDBACK_OPTIONS.DISMISS]}:{' '}
|
||||
</Text>
|
||||
<Text>{t('Dismiss')}</Text>
|
||||
<Text> </Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ vi.mock('../../i18n/index.js', () => ({
|
|||
en: 'English',
|
||||
ru: 'Russian',
|
||||
de: 'German',
|
||||
ja: 'Japanese',
|
||||
pt: 'Portuguese',
|
||||
};
|
||||
return map[locale] || 'English';
|
||||
}),
|
||||
|
|
@ -72,6 +74,7 @@ vi.mock('@qwen-code/qwen-code-core', async (importOriginal) => {
|
|||
|
||||
// Import modules after mocking
|
||||
import * as i18n from '../../i18n/index.js';
|
||||
import { SUPPORTED_LANGUAGES } from '../../i18n/languages.js';
|
||||
import { languageCommand } from './languageCommand.js';
|
||||
import { initializeLlmOutputLanguage } from '../../utils/languageUtils.js';
|
||||
|
||||
|
|
@ -565,10 +568,9 @@ describe('languageCommand', () => {
|
|||
|
||||
it('should have nested language subcommands', () => {
|
||||
const nestedNames = uiSubcommand?.subCommands?.map((c) => c.name);
|
||||
expect(nestedNames).toContain('zh-CN');
|
||||
expect(nestedNames).toContain('en-US');
|
||||
expect(nestedNames).toContain('ru-RU');
|
||||
expect(nestedNames).toContain('de-DE');
|
||||
for (const lang of SUPPORTED_LANGUAGES) {
|
||||
expect(nestedNames).toContain(lang.id);
|
||||
}
|
||||
});
|
||||
|
||||
it('should have action that sets language', async () => {
|
||||
|
|
@ -678,6 +680,24 @@ describe('languageCommand', () => {
|
|||
});
|
||||
});
|
||||
|
||||
const jaJPSubcommand = uiSubcommand?.subCommands?.find(
|
||||
(c) => c.name === 'ja-JP',
|
||||
);
|
||||
it('ja-JP action should set Japanese', async () => {
|
||||
if (!jaJPSubcommand?.action) {
|
||||
throw new Error('ja-JP subcommand must have an action.');
|
||||
}
|
||||
|
||||
const result = await jaJPSubcommand.action(mockContext, '');
|
||||
|
||||
expect(i18n.setLanguageAsync).toHaveBeenCalledWith('ja');
|
||||
expect(result).toEqual({
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content: expect.stringContaining('UI language changed'),
|
||||
});
|
||||
});
|
||||
|
||||
it('should reject extra arguments', async () => {
|
||||
if (!zhCNSubcommand?.action) {
|
||||
throw new Error('zh-CN subcommand must have an action.');
|
||||
|
|
@ -798,5 +818,31 @@ describe('languageCommand', () => {
|
|||
'utf-8',
|
||||
);
|
||||
});
|
||||
|
||||
it('should detect Japanese locale and create Japanese rule file', () => {
|
||||
vi.mocked(fs.existsSync).mockReturnValue(false);
|
||||
vi.mocked(i18n.detectSystemLanguage).mockReturnValue('ja');
|
||||
|
||||
initializeLlmOutputLanguage();
|
||||
|
||||
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
||||
expect.stringContaining('output-language.md'),
|
||||
expect.stringContaining('Japanese'),
|
||||
'utf-8',
|
||||
);
|
||||
});
|
||||
|
||||
it('should detect Portuguese locale and create Portuguese rule file', () => {
|
||||
vi.mocked(fs.existsSync).mockReturnValue(false);
|
||||
vi.mocked(i18n.detectSystemLanguage).mockReturnValue('pt');
|
||||
|
||||
initializeLlmOutputLanguage();
|
||||
|
||||
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
||||
expect.stringContaining('output-language.md'),
|
||||
expect.stringContaining('Portuguese'),
|
||||
'utf-8',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -18,7 +18,10 @@ import {
|
|||
type SupportedLanguage,
|
||||
t,
|
||||
} from '../../i18n/index.js';
|
||||
import { SUPPORTED_LANGUAGES } from '../../i18n/languages.js';
|
||||
import {
|
||||
SUPPORTED_LANGUAGES,
|
||||
getSupportedLanguageIds,
|
||||
} from '../../i18n/languages.js';
|
||||
import {
|
||||
OUTPUT_LANGUAGE_AUTO,
|
||||
isAutoLanguage,
|
||||
|
|
@ -62,11 +65,14 @@ function parseUiLanguageArg(input: string): SupportedLanguage | null {
|
|||
}
|
||||
|
||||
/**
|
||||
* Formats a UI language code for display (e.g., "zh" -> "Chinese(zh-CN)").
|
||||
* Formats a UI language code for display (e.g., "zh" -> "中文 (Chinese) [zh-CN]").
|
||||
*/
|
||||
function formatUiLanguageDisplay(lang: SupportedLanguage): string {
|
||||
const option = SUPPORTED_LANGUAGES.find((o) => o.code === lang);
|
||||
return option ? `${option.fullName}(${option.id})` : lang;
|
||||
if (!option) return lang;
|
||||
return option.nativeName && option.nativeName !== option.fullName
|
||||
? `${option.nativeName} (${option.fullName}) [${option.id}]`
|
||||
: `${option.fullName} [${option.id}]`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -219,7 +225,7 @@ export const languageCommand: SlashCommand = {
|
|||
messageType: 'error',
|
||||
content: [
|
||||
t('Invalid command. Available subcommands:'),
|
||||
` - /language ui [${SUPPORTED_LANGUAGES.map((o) => o.id).join('|')}] - ${t('Set UI language')}`,
|
||||
` - /language ui [${getSupportedLanguageIds()}] - ${t('Set UI language')}`,
|
||||
` - /language output <language> - ${t('Set LLM output language')}`,
|
||||
].join('\n'),
|
||||
};
|
||||
|
|
@ -245,7 +251,7 @@ export const languageCommand: SlashCommand = {
|
|||
t('Current LLM output language: {{lang}}', { lang: outputLangDisplay }),
|
||||
'',
|
||||
t('Available subcommands:'),
|
||||
` /language ui [${SUPPORTED_LANGUAGES.map((o) => o.id).join('|')}] - ${t('Set UI language')}`,
|
||||
` /language ui [${getSupportedLanguageIds()}] - ${t('Set UI language')}`,
|
||||
` /language output <language> - ${t('Set LLM output language')}`,
|
||||
].join('\n'),
|
||||
};
|
||||
|
|
@ -274,12 +280,12 @@ export const languageCommand: SlashCommand = {
|
|||
t('Set UI language'),
|
||||
'',
|
||||
t('Usage: /language ui [{{options}}]', {
|
||||
options: SUPPORTED_LANGUAGES.map((o) => o.id).join('|'),
|
||||
options: getSupportedLanguageIds(),
|
||||
}),
|
||||
'',
|
||||
t('Available options:'),
|
||||
...SUPPORTED_LANGUAGES.map(
|
||||
(o) => ` - ${o.id}: ${t(o.fullName)}`,
|
||||
(o) => ` - ${o.id}: ${o.nativeName || o.fullName}`,
|
||||
),
|
||||
'',
|
||||
t(
|
||||
|
|
@ -295,7 +301,7 @@ export const languageCommand: SlashCommand = {
|
|||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: t('Invalid language. Available: {{options}}', {
|
||||
options: SUPPORTED_LANGUAGES.map((o) => o.id).join(','),
|
||||
options: getSupportedLanguageIds(','),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
|
@ -308,7 +314,9 @@ export const languageCommand: SlashCommand = {
|
|||
(lang): SlashCommand => ({
|
||||
name: lang.id,
|
||||
get description() {
|
||||
return t('Set UI language to {{name}}', { name: lang.fullName });
|
||||
return t('Set UI language to {{name}}', {
|
||||
name: lang.nativeName || lang.fullName,
|
||||
});
|
||||
},
|
||||
kind: CommandKind.BUILT_IN,
|
||||
action: async (context, args) => {
|
||||
|
|
|
|||
|
|
@ -6,22 +6,21 @@
|
|||
|
||||
import { Text } from 'ink';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
import { tokenLimit } from '@qwen-code/qwen-code-core';
|
||||
|
||||
export const ContextUsageDisplay = ({
|
||||
promptTokenCount,
|
||||
model,
|
||||
terminalWidth,
|
||||
contextWindowSize,
|
||||
}: {
|
||||
promptTokenCount: number;
|
||||
model: string;
|
||||
terminalWidth: number;
|
||||
contextWindowSize: number;
|
||||
}) => {
|
||||
if (promptTokenCount === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const percentage = promptTokenCount / tokenLimit(model);
|
||||
const percentage = promptTokenCount / contextWindowSize;
|
||||
const percentageUsed = (percentage * 100).toFixed(1);
|
||||
|
||||
const label = terminalWidth < 100 ? '% used' : '% context used';
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ const defaultProps = {
|
|||
const createMockConfig = (overrides = {}) => ({
|
||||
getModel: vi.fn(() => defaultProps.model),
|
||||
getDebugMode: vi.fn(() => false),
|
||||
getContentGeneratorConfig: vi.fn(() => ({ contextWindowSize: 131072 })),
|
||||
getMcpServers: vi.fn(() => ({})),
|
||||
getBlockedMcpServers: vi.fn(() => []),
|
||||
...overrides,
|
||||
|
|
|
|||
|
|
@ -26,13 +26,11 @@ export const Footer: React.FC = () => {
|
|||
const { vimEnabled, vimMode } = useVimMode();
|
||||
|
||||
const {
|
||||
model,
|
||||
errorCount,
|
||||
showErrorDetails,
|
||||
promptTokenCount,
|
||||
showAutoAcceptIndicator,
|
||||
} = {
|
||||
model: config.getModel(),
|
||||
errorCount: uiState.errorCount,
|
||||
showErrorDetails: uiState.showErrorDetails,
|
||||
promptTokenCount: uiState.sessionStats.lastPromptTokenCount,
|
||||
|
|
@ -57,6 +55,9 @@ export const Footer: React.FC = () => {
|
|||
// Check if debug mode is enabled
|
||||
const debugMode = config.getDebugMode();
|
||||
|
||||
const contextWindowSize =
|
||||
config.getContentGeneratorConfig()?.contextWindowSize;
|
||||
|
||||
// Left section should show exactly ONE thing at any time, in priority order.
|
||||
const leftContent = uiState.ctrlCPressedOnce ? (
|
||||
<Text color={theme.status.warning}>{t('Press Ctrl+C again to exit.')}</Text>
|
||||
|
|
@ -88,15 +89,15 @@ export const Footer: React.FC = () => {
|
|||
node: <Text color={theme.status.warning}>Debug Mode</Text>,
|
||||
});
|
||||
}
|
||||
if (promptTokenCount > 0) {
|
||||
if (promptTokenCount > 0 && contextWindowSize) {
|
||||
rightItems.push({
|
||||
key: 'context',
|
||||
node: (
|
||||
<Text color={theme.text.accent}>
|
||||
<ContextUsageDisplay
|
||||
promptTokenCount={promptTokenCount}
|
||||
model={model}
|
||||
terminalWidth={terminalWidth}
|
||||
contextWindowSize={contextWindowSize}
|
||||
/>
|
||||
</Text>
|
||||
),
|
||||
|
|
|
|||
|
|
@ -36,6 +36,11 @@ vi.mock('../utils/clipboardUtils.js');
|
|||
vi.mock('../contexts/UIStateContext.js', () => ({
|
||||
useUIState: vi.fn(() => ({ isFeedbackDialogOpen: false })),
|
||||
}));
|
||||
vi.mock('../contexts/UIActionsContext.js', () => ({
|
||||
useUIActions: vi.fn(() => ({
|
||||
temporaryCloseFeedbackDialog: vi.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
const mockSlashCommands: SlashCommand[] = [
|
||||
{
|
||||
|
|
@ -376,7 +381,7 @@ describe('InputPrompt', () => {
|
|||
it('should handle Ctrl+V when clipboard has an image', async () => {
|
||||
vi.mocked(clipboardUtils.clipboardHasImage).mockResolvedValue(true);
|
||||
vi.mocked(clipboardUtils.saveClipboardImage).mockResolvedValue(
|
||||
'/test/.gemini-clipboard/clipboard-123.png',
|
||||
'/test/.qwen-clipboard/clipboard-123.png',
|
||||
);
|
||||
|
||||
const { stdin, unmount } = renderWithProviders(
|
||||
|
|
@ -436,7 +441,7 @@ describe('InputPrompt', () => {
|
|||
it('should insert image path at cursor position with proper spacing', async () => {
|
||||
const imagePath = path.join(
|
||||
'test',
|
||||
'.gemini-clipboard',
|
||||
'.qwen-clipboard',
|
||||
'clipboard-456.png',
|
||||
);
|
||||
vi.mocked(clipboardUtils.clipboardHasImage).mockResolvedValue(true);
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ import * as path from 'node:path';
|
|||
import { SCREEN_READER_USER_PREFIX } from '../textConstants.js';
|
||||
import { useShellFocusState } from '../contexts/ShellFocusContext.js';
|
||||
import { useUIState } from '../contexts/UIStateContext.js';
|
||||
import { useUIActions } from '../contexts/UIActionsContext.js';
|
||||
import { FEEDBACK_DIALOG_KEYS } from '../FeedbackDialog.js';
|
||||
export interface InputPromptProps {
|
||||
buffer: TextBuffer;
|
||||
|
|
@ -109,6 +110,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
|||
}) => {
|
||||
const isShellFocused = useShellFocusState();
|
||||
const uiState = useUIState();
|
||||
const uiActions = useUIActions();
|
||||
const [justNavigatedHistory, setJustNavigatedHistory] = useState(false);
|
||||
const [escPressCount, setEscPressCount] = useState(0);
|
||||
const [showEscapePrompt, setShowEscapePrompt] = useState(false);
|
||||
|
|
@ -337,12 +339,16 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
|||
return;
|
||||
}
|
||||
|
||||
// Intercept feedback dialog option keys (1, 2) when dialog is open
|
||||
if (
|
||||
uiState.isFeedbackDialogOpen &&
|
||||
(FEEDBACK_DIALOG_KEYS as readonly string[]).includes(key.name)
|
||||
) {
|
||||
return;
|
||||
// Handle feedback dialog keyboard interactions when dialog is open
|
||||
if (uiState.isFeedbackDialogOpen) {
|
||||
// If it's one of the feedback option keys (1-4), let FeedbackDialog handle it
|
||||
if ((FEEDBACK_DIALOG_KEYS as readonly string[]).includes(key.name)) {
|
||||
return;
|
||||
} else {
|
||||
// For any other key, close feedback dialog temporarily and continue with normal processing
|
||||
uiActions.temporaryCloseFeedbackDialog();
|
||||
// Continue processing the key for normal input handling
|
||||
}
|
||||
}
|
||||
|
||||
// Reset ESC count and hide prompt on any non-ESC key
|
||||
|
|
@ -712,6 +718,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
|||
onToggleShortcuts,
|
||||
showShortcuts,
|
||||
uiState,
|
||||
uiActions,
|
||||
],
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -1368,7 +1368,7 @@ describe('SettingsDialog', () => {
|
|||
enabled: true,
|
||||
},
|
||||
context: {
|
||||
loadMemoryFromIncludeDirectories: true,
|
||||
loadFromIncludeDirectories: true,
|
||||
fileFiltering: {
|
||||
respectGitIgnore: true,
|
||||
respectQwenIgnore: true,
|
||||
|
|
@ -1540,7 +1540,7 @@ describe('SettingsDialog', () => {
|
|||
enableRecursiveFileSearch: false,
|
||||
disableFuzzySearch: true,
|
||||
},
|
||||
loadMemoryFromIncludeDirectories: true,
|
||||
loadFromIncludeDirectories: true,
|
||||
},
|
||||
});
|
||||
const onSelect = vi.fn();
|
||||
|
|
@ -1605,7 +1605,7 @@ describe('SettingsDialog', () => {
|
|||
enabled: false,
|
||||
},
|
||||
context: {
|
||||
loadMemoryFromIncludeDirectories: false,
|
||||
loadFromIncludeDirectories: false,
|
||||
fileFiltering: {
|
||||
respectGitIgnore: false,
|
||||
respectQwenIgnore: false,
|
||||
|
|
|
|||
|
|
@ -71,6 +71,7 @@ export interface UIActions {
|
|||
// Feedback dialog
|
||||
openFeedbackDialog: () => void;
|
||||
closeFeedbackDialog: () => void;
|
||||
temporaryCloseFeedbackDialog: () => void;
|
||||
submitFeedback: (rating: number) => void;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import {
|
|||
USER_SETTINGS_PATH,
|
||||
} from '../../config/settings.js';
|
||||
import type { SessionStatsState } from '../contexts/SessionContext.js';
|
||||
import { FEEDBACK_OPTIONS } from '../FeedbackDialog.js';
|
||||
import stripJsonComments from 'strip-json-comments';
|
||||
|
||||
const FEEDBACK_SHOW_PROBABILITY = 0.25; // 25% probability of showing feedback dialog
|
||||
|
|
@ -96,37 +97,48 @@ export const useFeedbackDialog = ({
|
|||
}: UseFeedbackDialogProps) => {
|
||||
// Feedback dialog state
|
||||
const [isFeedbackDialogOpen, setIsFeedbackDialogOpen] = useState(false);
|
||||
const [isFeedbackDismissedTemporarily, setIsFeedbackDismissedTemporarily] =
|
||||
useState(false);
|
||||
|
||||
const openFeedbackDialog = useCallback(() => {
|
||||
setIsFeedbackDialogOpen(true);
|
||||
|
||||
// Record the timestamp when feedback dialog is shown (fire and forget)
|
||||
settings.setValue(
|
||||
SettingScope.User,
|
||||
'ui.feedbackLastShownTimestamp',
|
||||
Date.now(),
|
||||
);
|
||||
}, [settings]);
|
||||
}, []);
|
||||
|
||||
const closeFeedbackDialog = useCallback(
|
||||
() => setIsFeedbackDialogOpen(false),
|
||||
[],
|
||||
);
|
||||
|
||||
const temporaryCloseFeedbackDialog = useCallback(() => {
|
||||
setIsFeedbackDialogOpen(false);
|
||||
setIsFeedbackDismissedTemporarily(true);
|
||||
}, []);
|
||||
|
||||
const submitFeedback = useCallback(
|
||||
(rating: number) => {
|
||||
// Create and log the feedback event
|
||||
const feedbackEvent = new UserFeedbackEvent(
|
||||
sessionStats.sessionId,
|
||||
rating as UserFeedbackRating,
|
||||
config.getModel(),
|
||||
config.getApprovalMode(),
|
||||
// Only create and log feedback event for ratings 1-3 (GOOD, BAD, FINE)
|
||||
// Rating 0 (DISMISS) should not trigger any telemetry
|
||||
if (rating >= FEEDBACK_OPTIONS.GOOD && rating <= FEEDBACK_OPTIONS.FINE) {
|
||||
const feedbackEvent = new UserFeedbackEvent(
|
||||
sessionStats.sessionId,
|
||||
rating as UserFeedbackRating,
|
||||
config.getModel(),
|
||||
config.getApprovalMode(),
|
||||
);
|
||||
|
||||
logUserFeedback(config, feedbackEvent);
|
||||
}
|
||||
|
||||
// Record the timestamp when feedback dialog is submitted
|
||||
settings.setValue(
|
||||
SettingScope.User,
|
||||
'ui.feedbackLastShownTimestamp',
|
||||
Date.now(),
|
||||
);
|
||||
|
||||
logUserFeedback(config, feedbackEvent);
|
||||
closeFeedbackDialog();
|
||||
},
|
||||
[config, sessionStats, closeFeedbackDialog],
|
||||
[closeFeedbackDialog, sessionStats.sessionId, config, settings],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -140,13 +152,15 @@ export const useFeedbackDialog = ({
|
|||
// 5. Random chance (25% probability)
|
||||
// 6. Meets minimum requirements (tool calls > 10 OR user messages > 5)
|
||||
// 7. Fatigue mechanism allows showing (not shown recently across sessions)
|
||||
// 8. Not temporarily dismissed
|
||||
if (
|
||||
config.getAuthType() !== AuthType.QWEN_OAUTH ||
|
||||
!config.getUsageStatisticsEnabled() ||
|
||||
settings.merged.ui?.enableUserFeedback === false ||
|
||||
!lastMessageIsAIResponse(history) ||
|
||||
Math.random() > FEEDBACK_SHOW_PROBABILITY ||
|
||||
!meetsMinimumSessionRequirements(sessionStats)
|
||||
!meetsMinimumSessionRequirements(sessionStats) ||
|
||||
isFeedbackDismissedTemporarily
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -164,15 +178,27 @@ export const useFeedbackDialog = ({
|
|||
history,
|
||||
sessionStats,
|
||||
isFeedbackDialogOpen,
|
||||
isFeedbackDismissedTemporarily,
|
||||
openFeedbackDialog,
|
||||
settings.merged.ui?.enableUserFeedback,
|
||||
config,
|
||||
]);
|
||||
|
||||
// Reset temporary dismissal when a new AI response starts streaming
|
||||
useEffect(() => {
|
||||
if (
|
||||
streamingState === StreamingState.Responding &&
|
||||
isFeedbackDismissedTemporarily
|
||||
) {
|
||||
setIsFeedbackDismissedTemporarily(false);
|
||||
}
|
||||
}, [streamingState, isFeedbackDismissedTemporarily]);
|
||||
|
||||
return {
|
||||
isFeedbackDialogOpen,
|
||||
openFeedbackDialog,
|
||||
closeFeedbackDialog,
|
||||
temporaryCloseFeedbackDialog,
|
||||
submitFeedback,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ export async function saveClipboardImage(
|
|||
// Create a temporary directory for clipboard images within the target directory
|
||||
// This avoids security restrictions on paths outside the target directory
|
||||
const baseDir = targetDir || process.cwd();
|
||||
const tempDir = path.join(baseDir, '.gemini-clipboard');
|
||||
const tempDir = path.join(baseDir, '.qwen-clipboard');
|
||||
await fs.mkdir(tempDir, { recursive: true });
|
||||
|
||||
// Generate a unique filename with timestamp
|
||||
|
|
@ -130,7 +130,7 @@ export async function cleanupOldClipboardImages(
|
|||
): Promise<void> {
|
||||
try {
|
||||
const baseDir = targetDir || process.cwd();
|
||||
const tempDir = path.join(baseDir, '.gemini-clipboard');
|
||||
const tempDir = path.join(baseDir, '.qwen-clipboard');
|
||||
const files = await fs.readdir(tempDir);
|
||||
const oneHourAgo = Date.now() - 60 * 60 * 1000;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue