feat(desktop): support clearing hotkey bindings in ShortcutManager (#12727)

* 🔧 chore: update @lobehub/ui dependency to a specific version URL and enhance ShortcutManager functionality

- Updated @lobehub/ui dependency in package.json to a specific version URL.
- Improved ShortcutManager to handle empty accelerator bindings, allowing users to clear shortcuts.
- Updated tests to reflect changes in shortcut handling and added localization for clear binding messages in both Chinese and English.

Signed-off-by: Innei <tukon479@gmail.com>

* 🔧 chore: update @lobehub/ui dependency to version 5.4.0 in package.json

- Changed the @lobehub/ui dependency from a specific version URL to version 5.4.0 for improved stability and consistency.

Signed-off-by: Innei <tukon479@gmail.com>

* 🔧 chore: update @lobehub/ui dependency to use caret versioning in package.json

- Changed the @lobehub/ui dependency from a fixed version to caret versioning (^5.4.0) to allow for minor updates and improvements.

Signed-off-by: Innei <tukon479@gmail.com>

---------

Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
Innei
2026-03-05 21:23:58 +08:00
committed by GitHub
parent 92c70d2485
commit 5920500371
8 changed files with 89 additions and 29 deletions

View File

@@ -84,13 +84,23 @@ export class ShortcutManager {
}
// 2. Basic format validation
if (!accelerator || typeof accelerator !== 'string' || accelerator.trim() === '') {
if (typeof accelerator !== 'string') {
logger.error(`Invalid accelerator format: ${accelerator}`);
return { errorType: 'INVALID_FORMAT', success: false };
}
const trimmedAccelerator = accelerator.trim();
// Empty value means disable this shortcut binding
if (trimmedAccelerator === '') {
this.shortcutsConfig[id] = '';
this.saveShortcutsConfig();
this.registerConfiguredShortcuts();
return { success: true };
}
// Convert frontend format to Electron format
const convertedAccelerator = this.convertAcceleratorFormat(accelerator.trim());
const convertedAccelerator = this.convertAcceleratorFormat(trimmedAccelerator);
const cleanAccelerator = convertedAccelerator.toLowerCase();
logger.debug(`Converted accelerator from ${accelerator} to ${convertedAccelerator}`);
@@ -221,7 +231,7 @@ export class ShortcutManager {
// If no configuration, use default configuration
if (!config || Object.keys(config).length === 0) {
logger.debug('No shortcuts config found, using defaults');
this.shortcutsConfig = DEFAULT_SHORTCUTS_CONFIG;
this.shortcutsConfig = { ...DEFAULT_SHORTCUTS_CONFIG };
this.saveShortcutsConfig();
} else {
// Filter out invalid shortcuts that are not in DEFAULT_SHORTCUTS_CONFIG
@@ -257,7 +267,7 @@ export class ShortcutManager {
logger.debug('Loaded shortcuts config:', this.shortcutsConfig);
} catch (error) {
logger.error('Error loading shortcuts config:', error);
this.shortcutsConfig = DEFAULT_SHORTCUTS_CONFIG;
this.shortcutsConfig = { ...DEFAULT_SHORTCUTS_CONFIG };
this.saveShortcutsConfig();
}
}

View File

@@ -175,11 +175,17 @@ describe('ShortcutManager', () => {
expect(result.errorType).toBe('INVALID_ID');
});
it('should reject empty accelerator', () => {
it('should clear shortcut when accelerator is empty', () => {
const result = shortcutManager.updateShortcutConfig('showApp', '');
expect(result.success).toBe(false);
expect(result.errorType).toBe('INVALID_FORMAT');
expect(result.success).toBe(true);
expect(result.errorType).toBeUndefined();
expect(mockStoreManager.set).toHaveBeenCalledWith(
'shortcuts',
expect.objectContaining({
showApp: '',
}),
);
});
it('should reject accelerator without modifier keys', () => {

View File

@@ -207,6 +207,7 @@
"header.sessionDesc": "助理档案与会话偏好",
"header.sessionWithName": "会话设置 · {{name}}",
"header.title": "设置",
"hotkey.clearBinding": "取消绑定",
"hotkey.conflicts": "与现有快捷键冲突",
"hotkey.errors.CONFLICT": "快捷键冲突:该快捷键已被其他功能占用",
"hotkey.errors.INVALID_FORMAT": "快捷键格式无效:请使用正确的格式(如 CommandOrControl+E",

View File

@@ -239,7 +239,7 @@
"@lobehub/icons": "^5.0.0",
"@lobehub/market-sdk": "^0.31.3",
"@lobehub/tts": "^5.1.2",
"@lobehub/ui": "^5.0.0",
"@lobehub/ui": "^5.4.0",
"@modelcontextprotocol/sdk": "^1.26.0",
"@napi-rs/canvas": "^0.1.88",
"@neondatabase/serverless": "^1.0.2",

View File

@@ -218,6 +218,7 @@ export default {
'header.sessionDesc': 'Agent Profile and session preferences',
'header.sessionWithName': 'Session Settings · {{name}}',
'header.title': 'Settings',
'hotkey.clearBinding': 'Clear binding',
'hotkey.conflicts': 'Conflicts with existing hotkeys',
'hotkey.errors.CONFLICT': 'Hotkey conflict: This hotkey is already assigned to another function',
'hotkey.errors.INVALID_FORMAT':

View File

@@ -25,6 +25,19 @@ const HotkeySetting = memo(() => {
if (!isUserStateInit) return <Skeleton active paragraph={{ rows: 5 }} title={false} />;
const clearHotkeyBinding = async (id: HotkeyItem['id']) => {
if (!hotkey[id]) return;
setLoading(true);
form.setFieldValue(id, '');
try {
await setSettings({ hotkey: { [id]: '' } });
} finally {
setLoading(false);
}
};
const mapHotkeyItem = (item: HotkeyItem) => {
const hotkeyConflicts = Object.entries(hotkey)
.map(([key, value]) => {
@@ -36,10 +49,13 @@ const HotkeySetting = memo(() => {
return {
children: (
<HotkeyInput
allowClear={!item.nonEditable}
disabled={item.nonEditable}
hotkeyConflicts={hotkeyConflicts}
placeholder={t('hotkey.record')}
resetValue={item.keys}
texts={{ clear: t('hotkey.clearBinding') }}
onClear={() => void clearHotkeyBinding(item.id)}
/>
),
desc: hotkeyMeta[`${item.id}.desc`] ? t(`${item.id}.desc`, { ns: 'hotkey' }) : undefined,
@@ -66,8 +82,11 @@ const HotkeySetting = memo(() => {
variant={'filled'}
onValuesChange={async (values) => {
setLoading(true);
await setSettings({ hotkey: values });
setLoading(false);
try {
await setSettings({ hotkey: values });
} finally {
setLoading(false);
}
}}
{...FORM_STYLE}
/>

View File

@@ -34,30 +34,34 @@ const HotkeySetting = memo(() => {
if (!isHotkeysInit) return <Skeleton active paragraph={{ rows: 5 }} title={false} />;
const updateHotkey = async (id: DesktopHotkeyItem['id'], value: string) => {
setLoading(true);
try {
const result = await updateDesktopHotkey(id, value);
if (result.success) {
message.success(t('hotkey.updateSuccess', { ns: 'setting' }));
} else {
// Show the appropriate error message based on error type
message.error(t(`hotkey.errors.${result.errorType}` as any, { ns: 'setting' }));
}
} catch {
message.error(t('hotkey.updateError', { ns: 'setting' }));
} finally {
setLoading(false);
}
};
const mapHotkeyItem = (item: DesktopHotkeyItem) => ({
children: (
<HotkeyInput
allowClear={!item.nonEditable}
disabled={item.nonEditable}
placeholder={t('hotkey.record')}
resetValue={item.keys}
texts={{ clear: t('hotkey.clearBinding') }}
value={hotkeys[item.id]}
onChange={async (value) => {
setLoading(true);
try {
const result = await updateDesktopHotkey(item.id, value);
if (result.success) {
message.success(t('hotkey.updateSuccess', { ns: 'setting' }));
} else {
// Show the appropriate error message based on error type
message.error(t(`hotkey.errors.${result.errorType}` as any, { ns: 'setting' }));
}
} catch {
message.error(t('hotkey.updateError', { ns: 'setting' }));
} finally {
setLoading(false);
}
}}
onChange={(value) => void updateHotkey(item.id, value)}
onClear={() => void updateHotkey(item.id, '')}
/>
),

View File

@@ -25,6 +25,19 @@ const HotkeySetting = memo(() => {
if (!isUserStateInit) return <Skeleton active paragraph={{ rows: 5 }} title={false} />;
const clearHotkeyBinding = async (id: HotkeyItem['id']) => {
if (!hotkey[id]) return;
setLoading(true);
form.setFieldValue(id, '');
try {
await setSettings({ hotkey: { [id]: '' } });
} finally {
setLoading(false);
}
};
const mapHotkeyItem = (item: HotkeyItem) => {
const hotkeyConflicts = Object.entries(hotkey)
.map(([key, value]) => {
@@ -36,10 +49,13 @@ const HotkeySetting = memo(() => {
return {
children: (
<HotkeyInput
allowClear={!item.nonEditable}
disabled={item.nonEditable}
hotkeyConflicts={hotkeyConflicts}
placeholder={t('hotkey.record')}
resetValue={item.keys}
texts={{ clear: t('hotkey.clearBinding') }}
onClear={() => void clearHotkeyBinding(item.id)}
/>
),
desc: hotkeyMeta[`${item.id}.desc`] ? t(`${item.id}.desc`, { ns: 'hotkey' }) : undefined,
@@ -66,8 +82,11 @@ const HotkeySetting = memo(() => {
variant={'filled'}
onValuesChange={async (values) => {
setLoading(true);
await setSettings({ hotkey: values });
setLoading(false);
try {
await setSettings({ hotkey: values });
} finally {
setLoading(false);
}
}}
{...FORM_STYLE}
/>