Merge pull request #7787 from deepseek-harness/turtle/add-paste-plain-shortcut-d16f8fd43c4b

feat(desktop): bind Paste and Match Style to Shift+Command+V on macOS
This commit is contained in:
std4453
2026-11-04 15:18:06 +08:00
committed by GitHub
6 changed files with 57 additions and 6 deletions
+2 -2
View File
@@ -33,8 +33,8 @@
en: 372297005b57a08b
zh: 074a635ac17e4362
/deepseek-harness-desktop/installation-ownership:
en: 160b500cdcd6a39a
zh: 6c267f1762ec3689
en: 575d82acb5d61018
zh: b1064786f63863f7
/deepseek-harness-desktop/installation-ownership/runtime-and-plugin-activation:
en: 4951cd3ee659eb33
zh: 96bdd6358dad587a
+1 -1
View File
@@ -161,7 +161,7 @@ Windows and macOS relay native fullscreen transitions and the state after each p
Windows uses a 40-DIP caption with native window controls and colors synchronized from the application palette. Localized Application and Edit entries beside the sidebar toggle open native popup menus. They mount only after the application frame publishes its shell overlay seat, and remain absent during startup loading. Application provides Check for Updates and Exit; Edit provides undo, redo, cut, copy, paste, delete, and select all by sending the corresponding keys to the focused editor, independently of custom shortcut bindings. Plugin management uses the main application's Plugins page. No separate native menu row appears on Alt. Other platforms retain their native menus. Editable fields retain keyboard commands and a context menu without shortcut labels; Chromium supplies command availability, and selected read-only text offers Copy.
On macOS the custom menu retains Electron's standard Window menu and application hide commands, including Minimize (⌘M) and Hide (⌘H). Linux keeps the application and Edit menus.
On macOS the custom menu retains Electron's standard Window menu and application hide commands, including Minimize (⌘M) and Hide (⌘H). Its Edit menu copies Electron's, binds Paste and Match Style to ⇧⌘V, and keeps Electron's ⌥⇧⌘V as a hidden alias; the Substitutions and Speech titles come from the desktop locale, while the other items keep Electron's English labels. Linux keeps the application and Edit menus.
### Runtime and plugin activation
+1 -1
View File
@@ -161,7 +161,7 @@ Windows 和 macOS 会在原生全屏切换及每次页面加载后向渲染器
Windows 使用 40 DIP 顶栏,保留原生窗口按钮,颜色随应用调色板同步。侧栏开关旁的本地化“应用”和“编辑”入口打开原生弹出菜单。仅当应用框架发布 shell overlay 席位后才挂载菜单,启动加载期间不显示。“应用”提供检查更新和退出;“编辑”向当前编辑器发送对应按键,提供撤销、重做、剪切、复制、粘贴、删除和全选,不受自定义快捷键绑定影响。插件管理使用主应用的“插件”页面。按 Alt 不会出现额外的原生菜单行。其他平台保留原生菜单。可编辑区域保留快捷键和不带快捷键标注的右键菜单;命令可用状态由 Chromium 提供,选中的只读文本提供“复制”命令。
macOS 上自定义菜单保留 Electron 的标准 Window 菜单及应用隐藏命令,包括 Minimize(⌘M)和 Hide(⌘H)。Linux 保留应用菜单和 Edit 菜单。
macOS 上自定义菜单保留 Electron 的标准 Window 菜单及应用隐藏命令,包括 Minimize(⌘M)和 Hide(⌘H)。其 Edit 菜单复制 Electron 的默认菜单,把 Paste and Match Style 绑定到 ⇧⌘V,并保留 Electron 默认的 ⌥⇧⌘V 作为隐藏别名;Substitutions 和 Speech 标题取自桌面端语言包,其余条目保留 Electron 的英文标签。Linux 保留应用菜单和 Edit 菜单。
### 运行时与插件激活
+16
View File
@@ -37,6 +37,8 @@ export const en = {
hideApplication: 'Hide DeepSeek Harness',
hideOtherApplications: 'Hide Others',
showAllApplications: 'Show All',
substitutionsMenu: 'Substitutions',
speechMenu: 'Speech',
quitApplication: 'Quit DeepSeek Harness',
openApplication: 'Open DeepSeek Harness',
quit: 'Quit',
@@ -228,6 +230,8 @@ export const zh = {
hideApplication: '隐藏 DeepSeek Harness',
hideOtherApplications: '隐藏其他',
showAllApplications: '显示全部',
substitutionsMenu: '替换',
speechMenu: '语音',
quitApplication: '退出 DeepSeek Harness',
openApplication: '打开 DeepSeek Harness',
quit: '退出',
@@ -417,6 +421,8 @@ export const ja = {
hideApplication: 'DeepSeek Harness を隠す',
hideOtherApplications: 'ほかを隠す',
showAllApplications: 'すべて表示',
substitutionsMenu: '自動置換',
speechMenu: 'スピーチ',
quitApplication: 'DeepSeek Harness を終了',
openApplication: 'DeepSeek Harness を開く',
quit: '終了',
@@ -606,6 +612,8 @@ export const es = {
hideApplication: 'Ocultar DeepSeek Harness',
hideOtherApplications: 'Ocultar otros',
showAllApplications: 'Mostrar todo',
substitutionsMenu: 'Sustituciones',
speechMenu: 'Voz',
quitApplication: 'Salir de DeepSeek Harness',
openApplication: 'Abrir DeepSeek Harness',
quit: 'Salir',
@@ -795,6 +803,8 @@ export const fr = {
hideApplication: 'Masquer DeepSeek Harness',
hideOtherApplications: 'Masquer les autres',
showAllApplications: 'Afficher tout',
substitutionsMenu: 'Substitutions',
speechMenu: 'Parole',
quitApplication: 'Quitter DeepSeek Harness',
openApplication: 'Ouvrir DeepSeek Harness',
quit: 'Quitter',
@@ -984,6 +994,8 @@ export const de = {
hideApplication: 'DeepSeek Harness ausblenden',
hideOtherApplications: 'Andere ausblenden',
showAllApplications: 'Alle einblenden',
substitutionsMenu: 'Ersetzungen',
speechMenu: 'Sprachausgabe',
quitApplication: 'DeepSeek Harness beenden',
openApplication: 'DeepSeek Harness öffnen',
quit: 'Beenden',
@@ -1173,6 +1185,8 @@ export const ru = {
hideApplication: 'Скрыть DeepSeek Harness',
hideOtherApplications: 'Скрыть других',
showAllApplications: 'Показать все',
substitutionsMenu: 'Замены',
speechMenu: 'Речь',
quitApplication: 'Выйти из DeepSeek Harness',
openApplication: 'Открыть DeepSeek Harness',
quit: 'Выйти',
@@ -1362,6 +1376,8 @@ export const pt = {
hideApplication: 'Ocultar DeepSeek Harness',
hideOtherApplications: 'Ocultar outros',
showAllApplications: 'Mostrar tudo',
substitutionsMenu: 'Substituições',
speechMenu: 'Fala',
quitApplication: 'Sair de DeepSeek Harness',
openApplication: 'Abra DeepSeek Harness',
quit: 'Sair',
+25 -1
View File
@@ -37,6 +37,7 @@ import { DesktopBackendController } from './backend-controller.ts'
import { DESKTOP_IPC, SCHEME, assertDesktopSender, type DesktopUpdateState } from './ipc.ts'
import { readDeviceInfo } from './device-info.ts'
import { desktopUpdateReadyConfirmation, formatDesktopMessage, resolveDesktopLocale, resolveDesktopStartupLocale } from './locale.ts'
import type { DesktopMessages } from './locale.ts'
import { claimDesktopSingleInstance } from './single-instance.ts'
import { desktopUpdateChannel } from './update-channel.ts'
import { DesktopUpdateCoordinator } from './update-coordinator.ts'
@@ -1141,8 +1142,31 @@ async function main(): Promise<void> {
// its standard menus and application hide commands declared explicitly.
// Keep app.name stable: Electron derives its default userData directory from it.
const darwin = process.platform === 'darwin'
// A copy of Electron 44's macOS Edit menu that binds Paste and Match Style to Shift+Command+V
// (#7782) and keeps Electron's Option+Shift+Command+V as a hidden alias.
const macEditMenu = (messages: DesktopMessages): MenuItemConstructorOptions => ({ role: 'editMenu', submenu: [
{ role: 'undo' },
{ role: 'redo' },
{ type: 'separator' },
{ role: 'cut' },
{ role: 'copy' },
{ role: 'paste' },
{ role: 'pasteAndMatchStyle', accelerator: 'Shift+Command+V' },
{ role: 'pasteAndMatchStyle', accelerator: 'Option+Shift+Command+V', visible: false, acceleratorWorksWhenHidden: true },
{ role: 'delete' },
{ role: 'selectAll' },
{ type: 'separator' },
{ label: messages.substitutionsMenu, submenu: [
{ role: 'showSubstitutions' },
{ type: 'separator' },
{ role: 'toggleSmartQuotes' },
{ role: 'toggleSmartDashes' },
{ role: 'toggleTextReplacement' },
] },
{ label: messages.speechMenu, submenu: [{ role: 'startSpeaking' }, { role: 'stopSpeaking' }] },
] })
const platformMenus = (): MenuItemConstructorOptions[] => darwin
? [shortcuts.fileMenu(currentDesktopLocale().messages), { role: 'editMenu' }, { role: 'windowMenu' }]
? [shortcuts.fileMenu(currentDesktopLocale().messages), macEditMenu(currentDesktopLocale().messages), { role: 'windowMenu' }]
: [{ role: 'editMenu' }]
const hideCommands: MenuItemConstructorOptions[] = darwin
? [{ role: 'hide', label: currentDesktopLocale().messages.hideApplication },
+12 -1
View File
@@ -1328,7 +1328,7 @@ describe('desktop main startup', () => {
await edit
})
it.each(['darwin', 'linux'] as const)('adds the product File menu and standard window commands only on macOS (%s)', async (platform) => {
it.each(['darwin', 'linux'] as const)('declares the platform application menus, with the product File, Edit, and Window menus on macOS (%s)', async (platform) => {
vi.spyOn(process, 'platform', 'get').mockReturnValue(platform)
await import('../src/main.ts')
await harness.preparing.promise
@@ -1341,6 +1341,17 @@ describe('desktop main startup', () => {
expect(template.map(describeItem)).toEqual(platform === 'darwin'
? ['Desktop test', en.fileMenu, 'editMenu', 'windowMenu']
: ['Application', 'editMenu'])
const describeEdit = (items: MenuItemConstructorOptions[]): unknown[] => items.map(item => Array.isArray(item.submenu)
? [describeItem(item), describeEdit(item.submenu)]
: [describeItem(item), item.accelerator, item.visible, item.acceleratorWorksWhenHidden].filter(value => value !== undefined))
const edit = template.find(item => item.role === 'editMenu')!.submenu as MenuItemConstructorOptions[] | undefined
expect(edit === undefined ? undefined : describeEdit(edit)).toEqual(platform === 'darwin'
? [['undo'], ['redo'], ['separator'], ['cut'], ['copy'], ['paste'],
['pasteAndMatchStyle', 'Shift+Command+V'], ['pasteAndMatchStyle', 'Option+Shift+Command+V', false, true],
['delete'], ['selectAll'], ['separator'],
[en.substitutionsMenu, [['showSubstitutions'], ['separator'], ['toggleSmartQuotes'], ['toggleSmartDashes'], ['toggleTextReplacement']]],
[en.speechMenu, [['startSpeaking'], ['stopSpeaking']]]]
: undefined)
const application = template[0]!.submenu as MenuItemConstructorOptions[]
expect(application.filter(item => item.visible !== false).map(describeItem)).toEqual(platform === 'darwin'
? ['about', 'separator', en.checkUpdatesMenu, en.cliCommandMenu, 'separator', 'toggleDevTools', 'separator', 'hide', 'hideOthers', 'unhide', 'separator', 'quit']