fix(shortcuts): integrate current desktop guests and client APIs
This commit is contained in:
1 parent
069c370987
commit
7b564f61e7
43 files changed
+468
-172
No files matched your search
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-09-16-desktop-window-menus.md
|
||||
2026-09-16-desktop-window-menus.md: 8a5343404e0cb3d2e7c4e20248a8b29f3bfbec47
|
||||
2026-09-16-desktop-window-menus.zh.md: d6ee6412462aecf49557a7994a683d37bf8e7253
|
||||
2026-09-16-desktop-window-menus.md: 34eff83707440719182f4ebe61aa552919bd44ee
|
||||
2026-09-16-desktop-window-menus.zh.md: c3cb3e5d89497ae15210c034c0077fc8019e5688
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write apps/desktop/README.md
|
||||
README.md: 66e94a8d334f5c48b387bf6f5d7d5a7af60e2d46
|
||||
README.zh.md: 5a765a09c217babe70f9ec4fc224b7442a4ce192
|
||||
README.md: 813813cf2292a6cfece78a273dcf6843093a95cf
|
||||
README.zh.md: e63557e98c0a19ecd9bf9e6c9497ce074ce8b57b
|
||||
@@ -24,7 +24,7 @@ The original artwork lives in `resources/icon.png` and `resources/icon.svg`; pla
|
||||
|
||||
Shortcut overrides are stored in `app.getPath('userData')/keybindings.json`, separately from `DSH_HOME`. The main process validates and serializes changes before publishing accepted bindings. Failed reads retain the last accepted bindings and block edits, including Restore All; unreadable and future-version files remain unchanged. Development can isolate these preferences with `DSH_DESKTOP_USER_DATA_DIR`; the launcher prints its resolved path. See the [shortcut service](../../packages/client/shortcuts/README.md) for format and conflict semantics.
|
||||
|
||||
The macOS File menu displays the accepted single-key binding, including arrow keys, and routes Close Page or Window through the client page owner. On Windows and macOS, all complete accepted bindings are intercepted before main-document and embedded-frame input, including editing and terminal input. Recording and input-method composition remain protected. Two-key chords leave the first key’s initial press available to the page and do not intercept its release; complete chords and their repeats are consumed. The renderer leaves configurable binding dispatch to the native adapter. Chords have no native menu accelerator. Accepted commands are forwarded once through the trusted preload. Linux dispatches main-document shortcuts through the DOM and forwards accepted embedded-frame bindings to the client resolver. Closing the last window preserves macOS application lifetime; Windows exits the desktop instance and stops its tasks. The [close decision](../../.agents/notes/implemented/architecture/2026-09-21-desktop-page-close-shortcuts.md) records this lifecycle choice.
|
||||
The macOS File menu displays the accepted single-key binding, including arrow keys, and routes Close Page or Window through the client page owner. On Windows and macOS, all complete accepted bindings are intercepted before main-document, embedded-frame, and browser-guest input, including editing and terminal input. Recording and input-method composition remain protected. Two-key chords leave the first key’s initial press available to the page and do not intercept its release; complete chords and their repeats are consumed. The renderer leaves configurable binding dispatch to the native adapter. Chords have no native menu accelerator. Accepted commands are forwarded once through the trusted preload. Linux dispatches main-document shortcuts through the DOM and forwards accepted embedded-frame bindings to the client resolver. Closing the last window preserves macOS application lifetime; Windows exits the desktop instance and stops its tasks. The [close decision](../../.agents/notes/implemented/architecture/2026-09-21-desktop-page-close-shortcuts.md) records this lifecycle choice.
|
||||
|
||||
The macOS PNG uses an inset rounded background for legacy ICNS packaging, with representations up to 1024 pixels. It is a flattened icon, not an Icon Composer document. Apple's [app icon guidance](https://developer.apple.com/design/human-interface-guidelines/app-icons) describes unmasked layers for Icon Composer; those inputs require a separate macOS export and must not reuse the rounded ICNS artwork. Verify Finder and Dock appearance on supported macOS versions before release.
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ Desktop Host 的 Platform API 请求与更新策略请求使用相同的 `x-clie
|
||||
|
||||
快捷键覆盖保存在 `app.getPath('userData')/keybindings.json`,与 `DSH_HOME` 分离。主进程校验并串行保存修改后才发布已接受键位。读取失败保留上次接受的键位并阻止编辑,包括全部恢复;不可读和未来版本的文件保持不变。开发时可通过 `DSH_DESKTOP_USER_DATA_DIR` 隔离这些偏好,启动器会输出解析后的路径。格式和冲突语义见[快捷键服务](../../packages/client/shortcuts/README.zh.md)。
|
||||
|
||||
macOS“文件”菜单显示已接受的单键绑定(包括方向键),并通过 Client 页面 owner 路由“关闭页面或窗口”。Windows 和 macOS 在主文档和内嵌 frame 输入之前拦截所有已接受的完整绑定,包括编辑和终端输入。录制和输入法组合状态仍受保护。双键组合会将首键的初次按下事件交给页面,且不拦截其松开事件;完整组合及其重复事件会被消费。渲染进程将可配置绑定的分发交给原生适配器。双键组合不注册原生菜单快捷键。已接受的命令通过可信 preload 转发一次。Linux 通过 DOM 分发主文档快捷键,并将已接受的内嵌 frame 绑定转发给 Client 解析器。关闭最后一个窗口后 macOS 保留应用生命周期;Windows 退出桌面实例并停止其任务。[关窗决策](../../.agents/notes/implemented/architecture/2026-09-21-desktop-page-close-shortcuts.zh.md)记录了这一生命周期选择。
|
||||
macOS“文件”菜单显示已接受的单键绑定(包括方向键),并通过 Client 页面 owner 路由“关闭页面或窗口”。Windows 和 macOS 在主文档、内嵌 frame 和浏览器 guest 输入之前拦截所有已接受的完整绑定,包括编辑和终端输入。录制和输入法组合状态仍受保护。双键组合会将首键的初次按下事件交给页面,且不拦截其松开事件;完整组合及其重复事件会被消费。渲染进程将可配置绑定的分发交给原生适配器。双键组合不注册原生菜单快捷键。已接受的命令通过可信 preload 转发一次。Linux 通过 DOM 分发主文档快捷键,并将已接受的内嵌 frame 绑定转发给 Client 解析器。关闭最后一个窗口后 macOS 保留应用生命周期;Windows 退出桌面实例并停止其任务。[关窗决策](../../.agents/notes/implemented/architecture/2026-09-21-desktop-page-close-shortcuts.zh.md)记录了这一生命周期选择。
|
||||
|
||||
macOS PNG 使用带留白的圆角底板,供传统 ICNS 打包使用,包含最高 1024 像素的表示。它是扁平图标,并非 Icon Composer 文档。Apple 的[应用图标指南](https://developer.apple.com/design/human-interface-guidelines/app-icons)要求向 Icon Composer 提供未遮罩的图层;这些输入需要在 macOS 上单独导出,不能复用已做圆角的 ICNS 图案。发布前须在支持的 macOS 版本中验收 Finder 和 Dock 的显示效果。
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ interface GuestLease {
|
||||
readonly partition: string
|
||||
attached: boolean
|
||||
guest?: WebContents
|
||||
releaseInput?: () => void
|
||||
}
|
||||
|
||||
/** Owns workspace storage partitions independently from individual tab guests. */
|
||||
@@ -51,6 +52,7 @@ export class DesktopBrowserGuests {
|
||||
const lease = this.leases.get(key)
|
||||
if (lease === undefined) return
|
||||
if (lease.owner !== owner) throw new Error('desktop browser: guest belongs to another window')
|
||||
lease.releaseInput?.()
|
||||
this.leases.delete(key)
|
||||
const guest = lease.guest
|
||||
if (guest !== undefined && !guest.isDestroyed()) {
|
||||
@@ -63,8 +65,9 @@ export class DesktopBrowserGuests {
|
||||
/**
|
||||
* Install attachment checks before the application document can create a webview.
|
||||
* @param window - primary application window.
|
||||
* @param attachInput - attaches native input after guest ownership is verified and returns its disposer.
|
||||
*/
|
||||
bind(window: BrowserWindow): void {
|
||||
bind(window: BrowserWindow, attachInput: (guest: WebContents, name: DesktopBrowserLeaseId) => () => void): void {
|
||||
const owner = window.webContents
|
||||
owner.on('will-attach-webview', (event, preferences, params) => {
|
||||
const id = typeof params.src === 'string' && params.src.startsWith('about:blank#')
|
||||
@@ -102,7 +105,8 @@ export class DesktopBrowserGuests {
|
||||
}
|
||||
lease.guest = guest
|
||||
attachedLease = id
|
||||
guest.once('destroyed', () => { this.leases.delete(id) })
|
||||
lease.releaseInput = attachInput(guest, id)
|
||||
guest.once('destroyed', () => { lease.releaseInput?.(); this.leases.delete(id) })
|
||||
})
|
||||
guest.setWindowOpenHandler(({ url, postBody }) => {
|
||||
const lease = attachedLease === undefined ? undefined : this.leases.get(attachedLease)
|
||||
|
||||
+131
-104
@@ -1,7 +1,8 @@
|
||||
/** Product-window preference IPC and native menu interception during physical-key dispatch/recording. */
|
||||
import { ipcMain, type BrowserWindow, type IpcMainInvokeEvent, type Input, type MenuItemConstructorOptions } from 'electron'
|
||||
import { ipcMain, type BrowserWindow, type IpcMainInvokeEvent, type Input, type MenuItemConstructorOptions, type WebContents } from 'electron'
|
||||
import { bindingKey, effectiveShortcuts, presentBinding, parseShortcutDefinitions, parseShortcutEdit } from '@deepseek-ai/dsh-client-shortcuts/protocol'
|
||||
import type { NormalizedBinding, ShortcutConfigSnapshot, ShortcutDefinition, ShortcutPlatform, ShortcutRevision } from '@deepseek-ai/dsh-client-shortcuts/protocol'
|
||||
import type { DesktopBrowserLeaseId } from '@deepseek-ai/dsh-client-ui-sidebar-browser/types'
|
||||
import { desktopKeybindings } from './keybindings.ts'
|
||||
import { DESKTOP_IPC, assertDesktopSender } from './ipc.ts'
|
||||
|
||||
@@ -24,6 +25,14 @@ export function installDesktopShortcuts(
|
||||
*/
|
||||
sendEditingKey(keyCode: string, modifiers: Array<'control'>): void
|
||||
attach(window: BrowserWindow): void
|
||||
/**
|
||||
* Intercept an approved browser guest's keys until its lease ends.
|
||||
* @param window - owning product window.
|
||||
* @param guest - approved browser contents.
|
||||
* @param name - main-issued lease used as the webview element name.
|
||||
* @returns idempotent listener disposer.
|
||||
*/
|
||||
attachGuest(window: BrowserWindow, guest: WebContents, name: DesktopBrowserLeaseId): () => void
|
||||
dispose(): void
|
||||
} {
|
||||
let definitions: readonly ShortcutDefinition[] = []
|
||||
@@ -33,6 +42,7 @@ export function installDesktopShortcuts(
|
||||
let editingInput: BrowserWindow['webContents'] | undefined
|
||||
const scopedDesktop = platform === 'windows' || platform === 'macos'
|
||||
const disposers = new Set<() => void>()
|
||||
const guestInputs = new Map<WebContents, { window: BrowserWindow; reset(): void }>()
|
||||
const closeAccelerator = (): string | undefined => presentBinding(closeBinding, platform).aria
|
||||
?.replace('Meta+', 'Command+').replace(/Arrow(Up|Down|Left|Right)$/u, '$1')
|
||||
const sendMenuClose = (): void => {
|
||||
@@ -87,11 +97,128 @@ export function installDesktopShortcuts(
|
||||
if (expected !== revision || revision === undefined || recording || !window.isFocused() || !window.isEnabled()) return
|
||||
window.close()
|
||||
})
|
||||
function attachInput(window: BrowserWindow, contents: WebContents, guestName?: DesktopBrowserLeaseId): () => void {
|
||||
let deadKey = false
|
||||
const held = new Set<string>()
|
||||
const consumed = new Map<string, 'press' | 'repeat'>()
|
||||
let inputFrame: typeof contents.focusedFrame = null
|
||||
let inputRevision: ShortcutRevision | undefined
|
||||
const resetInput = (): void => {
|
||||
deadKey = false; held.clear(); consumed.clear(); inputFrame = null; inputRevision = undefined
|
||||
if (!contents.isDestroyed()) contents.setIgnoreMenuShortcuts(false)
|
||||
}
|
||||
if (guestName !== undefined) guestInputs.set(contents, { window, reset: resetInput })
|
||||
const resetWindow = (): void => {
|
||||
resetInput()
|
||||
for (const guest of guestInputs.values()) if (guest.window === window) guest.reset()
|
||||
}
|
||||
const clear = (): void => {
|
||||
resetInput()
|
||||
if (guestName !== undefined) return
|
||||
definitions = []; keys.clear(); recording = false
|
||||
persistence.setDefinitions(null)
|
||||
}
|
||||
const navigation = (event: Electron.Event<Electron.WebContentsDidStartNavigationEventParams>): void => {
|
||||
if (event.isMainFrame && !event.isSameDocument) clear()
|
||||
}
|
||||
const beforeInput = (event: Electron.Event, input: Input): void => {
|
||||
if (editingInput === contents) {
|
||||
held.clear()
|
||||
consumed.clear()
|
||||
contents.setIgnoreMenuShortcuts(true)
|
||||
return
|
||||
}
|
||||
if (window !== getWindow() || !window.isFocused() || !window.isEnabled() || revision === undefined
|
||||
|| (guestName !== undefined && !contents.isFocused())) {
|
||||
contents.setIgnoreMenuShortcuts(false)
|
||||
held.clear()
|
||||
consumed.clear()
|
||||
return
|
||||
}
|
||||
const modifiers = (['control', 'alt', 'shift', 'meta'] as const).filter(modifier => input[modifier])
|
||||
const key = bindingKey({ code: input.code, modifiers })
|
||||
const match = keys.has(key)
|
||||
const menuMatch = closeBinding !== null && closeBinding.secondCode === undefined && modifiers.join('+') === closeBinding.modifiers.join('+')
|
||||
&& input.key.toUpperCase() === (closeBinding.code.startsWith('Key') ? closeBinding.code.slice(3) : input.code === closeBinding.code ? input.key.toUpperCase() : '')
|
||||
contents.setIgnoreMenuShortcuts(recording || match || menuMatch)
|
||||
const frame = contents.focusedFrame
|
||||
const composing = input.isComposing || input.key === 'Dead' || deadKey || input.modifiers.includes('altgr')
|
||||
if (input.type === 'keyDown') deadKey = input.key === 'Dead'
|
||||
if (recording || composing || frame === null) { held.clear(); consumed.clear(); return }
|
||||
let binding: NormalizedBinding = { code: input.code, modifiers }
|
||||
let priority = false
|
||||
if (scopedDesktop) {
|
||||
if (frame !== inputFrame || inputRevision !== revision) { held.clear(); inputFrame = frame; inputRevision = revision }
|
||||
const modifierKey = /^(Control|Alt|Shift|Meta)(Left|Right)$/u.test(input.code)
|
||||
if (input.type === 'keyUp') {
|
||||
// A chord's first key reached the renderer, so its release must reach the same input handlers.
|
||||
if (consumed.get(input.code) === 'press') event.preventDefault()
|
||||
consumed.delete(input.code)
|
||||
held.delete(input.code)
|
||||
if (modifierKey) held.clear()
|
||||
return
|
||||
}
|
||||
if (!input.isAutoRepeat) consumed.delete(input.code)
|
||||
if (modifierKey) { held.clear(); return }
|
||||
if (input.isAutoRepeat && consumed.has(input.code) && !match) { event.preventDefault(); return }
|
||||
if (input.isAutoRepeat && !held.has(input.code) && !match) return
|
||||
held.add(input.code)
|
||||
const codes: [string, ...string[]] = [input.code, ...[...held].filter(value => value !== input.code)]
|
||||
codes.sort()
|
||||
const pair = { code: codes[0], ...(codes[1] === undefined ? {} : { secondCode: codes[1] }), modifiers }
|
||||
priority = keys.has(key)
|
||||
if (codes.length === 2 && keys.has(bindingKey(pair))) { binding = pair; priority = true }
|
||||
}
|
||||
const main = guestName === undefined && frame === contents.mainFrame
|
||||
if (!priority && (main || !match)) return
|
||||
event.preventDefault()
|
||||
if (input.type !== 'keyDown') return
|
||||
if (scopedDesktop) {
|
||||
if (!input.isAutoRepeat) {
|
||||
if (binding.secondCode !== undefined) {
|
||||
consumed.set(binding.code, 'repeat')
|
||||
consumed.set(binding.secondCode, 'repeat')
|
||||
}
|
||||
consumed.set(input.code, 'press')
|
||||
}
|
||||
// Electron can omit both keyups after interception; completed presses cannot seed another chord.
|
||||
held.clear()
|
||||
}
|
||||
let embedding = frame
|
||||
while (guestName === undefined && !main && embedding.parent !== null && embedding.parent !== contents.mainFrame) {
|
||||
embedding = embedding.parent
|
||||
}
|
||||
window.webContents.send(DESKTOP_IPC.shortcutsInput, { kind: guestName === undefined ? main ? 'keyboard' : 'iframe' : 'webview',
|
||||
revision, frameName: guestName ?? (main ? '' : embedding.name),
|
||||
code: binding.code, ...(binding.secondCode === undefined ? {} : { secondCode: binding.secondCode }),
|
||||
repeat: input.isAutoRepeat, control: input.control, alt: input.alt, shift: input.shift, meta: input.meta })
|
||||
}
|
||||
const dispose = (): void => {
|
||||
contents.off('did-start-navigation', navigation)
|
||||
contents.off('before-input-event', beforeInput)
|
||||
contents.off('blur', resetInput)
|
||||
contents.off('destroyed', dispose)
|
||||
if (guestName === undefined) { window.off('blur', resetWindow); window.off('closed', closed) }
|
||||
disposers.delete(dispose)
|
||||
guestInputs.delete(contents)
|
||||
if (!contents.isDestroyed()) contents.setIgnoreMenuShortcuts(false)
|
||||
}
|
||||
const closed = (): void => { clear(); dispose() }
|
||||
contents.on('did-start-navigation', navigation)
|
||||
contents.on('before-input-event', beforeInput)
|
||||
contents.on('blur', resetInput)
|
||||
contents.once('destroyed', dispose)
|
||||
if (guestName === undefined) { window.on('closed', closed); window.on('blur', resetWindow) }
|
||||
disposers.add(dispose)
|
||||
return dispose
|
||||
}
|
||||
|
||||
return {
|
||||
sendEditingKey(keyCode, modifiers) {
|
||||
const window = getWindow()
|
||||
if (window === undefined || window.isDestroyed()) return
|
||||
const contents = window.webContents
|
||||
const contents = [...guestInputs].find(([guest, owner]) => owner.window === window && !guest.isDestroyed() && guest.isFocused())?.[0]
|
||||
?? window.webContents
|
||||
contents.focus()
|
||||
const previous = editingInput
|
||||
editingInput = contents
|
||||
@@ -112,108 +239,8 @@ export function installDesktopShortcuts(
|
||||
click: sendMenuClose,
|
||||
}] }
|
||||
},
|
||||
attach(window) {
|
||||
const contents = window.webContents
|
||||
let deadKey = false
|
||||
const held = new Set<string>()
|
||||
const consumed = new Map<string, 'press' | 'repeat'>()
|
||||
let inputFrame: typeof contents.focusedFrame = null
|
||||
let inputRevision: ShortcutRevision | undefined
|
||||
const clear = (): void => {
|
||||
definitions = []; keys.clear(); held.clear(); consumed.clear()
|
||||
inputFrame = null; recording = false; deadKey = false
|
||||
persistence.setDefinitions(null)
|
||||
if (!contents.isDestroyed()) contents.setIgnoreMenuShortcuts(false)
|
||||
}
|
||||
const navigation = (event: Electron.Event<Electron.WebContentsDidStartNavigationEventParams>): void => {
|
||||
if (event.isMainFrame && !event.isSameDocument) clear()
|
||||
}
|
||||
const blur = (): void => {
|
||||
deadKey = false; held.clear(); consumed.clear(); inputFrame = null; contents.setIgnoreMenuShortcuts(false)
|
||||
}
|
||||
const beforeInput = (event: Electron.Event, input: Input): void => {
|
||||
if (editingInput === contents) {
|
||||
held.clear()
|
||||
consumed.clear()
|
||||
contents.setIgnoreMenuShortcuts(true)
|
||||
return
|
||||
}
|
||||
if (!window.isFocused() || !window.isEnabled() || revision === undefined) {
|
||||
contents.setIgnoreMenuShortcuts(false)
|
||||
held.clear()
|
||||
consumed.clear()
|
||||
return
|
||||
}
|
||||
const modifiers = (['control', 'alt', 'shift', 'meta'] as const).filter(modifier => input[modifier])
|
||||
const key = bindingKey({ code: input.code, modifiers })
|
||||
const match = keys.has(key)
|
||||
const menuMatch = closeBinding !== null && closeBinding.secondCode === undefined && modifiers.join('+') === closeBinding.modifiers.join('+')
|
||||
&& input.key.toUpperCase() === (closeBinding.code.startsWith('Key') ? closeBinding.code.slice(3) : input.code === closeBinding.code ? input.key.toUpperCase() : '')
|
||||
contents.setIgnoreMenuShortcuts(recording || match || menuMatch)
|
||||
const frame = contents.focusedFrame
|
||||
const composing = input.isComposing || input.key === 'Dead' || deadKey || input.modifiers.includes('altgr')
|
||||
if (input.type === 'keyDown') deadKey = input.key === 'Dead'
|
||||
if (recording || composing || frame === null) { held.clear(); consumed.clear(); return }
|
||||
let binding: NormalizedBinding = { code: input.code, modifiers }
|
||||
let priority = false
|
||||
if (scopedDesktop) {
|
||||
if (frame !== inputFrame || inputRevision !== revision) { held.clear(); inputFrame = frame; inputRevision = revision }
|
||||
const modifierKey = /^(Control|Alt|Shift|Meta)(Left|Right)$/u.test(input.code)
|
||||
if (input.type === 'keyUp') {
|
||||
// A chord's first key reached the renderer, so its release must reach the same input handlers.
|
||||
if (consumed.get(input.code) === 'press') event.preventDefault()
|
||||
consumed.delete(input.code)
|
||||
held.delete(input.code)
|
||||
if (modifierKey) held.clear()
|
||||
return
|
||||
}
|
||||
if (!input.isAutoRepeat) consumed.delete(input.code)
|
||||
if (modifierKey) { held.clear(); return }
|
||||
if (input.isAutoRepeat && consumed.has(input.code) && !match) { event.preventDefault(); return }
|
||||
if (input.isAutoRepeat && !held.has(input.code) && !match) return
|
||||
held.add(input.code)
|
||||
const codes: [string, ...string[]] = [input.code, ...[...held].filter(value => value !== input.code)]
|
||||
codes.sort()
|
||||
const pair = { code: codes[0], ...(codes[1] === undefined ? {} : { secondCode: codes[1] }), modifiers }
|
||||
priority = keys.has(key)
|
||||
if (codes.length === 2 && keys.has(bindingKey(pair))) { binding = pair; priority = true }
|
||||
}
|
||||
const main = frame === contents.mainFrame
|
||||
if (!priority && (main || !match)) return
|
||||
event.preventDefault()
|
||||
if (input.type !== 'keyDown') return
|
||||
if (scopedDesktop) {
|
||||
if (!input.isAutoRepeat) {
|
||||
if (binding.secondCode !== undefined) {
|
||||
consumed.set(binding.code, 'repeat')
|
||||
consumed.set(binding.secondCode, 'repeat')
|
||||
}
|
||||
consumed.set(input.code, 'press')
|
||||
}
|
||||
// Electron can omit both keyups after interception; completed presses cannot seed another chord.
|
||||
held.clear()
|
||||
}
|
||||
let embedding = frame
|
||||
while (!main && embedding.parent !== null && embedding.parent !== contents.mainFrame) embedding = embedding.parent
|
||||
contents.send(DESKTOP_IPC.shortcutsInput, { kind: main ? 'keyboard' : 'iframe', revision, frameName: main ? '' : embedding.name,
|
||||
code: binding.code, ...(binding.secondCode === undefined ? {} : { secondCode: binding.secondCode }),
|
||||
repeat: input.isAutoRepeat, control: input.control, alt: input.alt, shift: input.shift, meta: input.meta })
|
||||
}
|
||||
const dispose = (): void => {
|
||||
contents.off('did-start-navigation', navigation)
|
||||
contents.off('before-input-event', beforeInput)
|
||||
window.off('blur', blur)
|
||||
window.off('closed', closed)
|
||||
disposers.delete(dispose)
|
||||
if (!contents.isDestroyed()) contents.setIgnoreMenuShortcuts(false)
|
||||
}
|
||||
const closed = (): void => { clear(); dispose() }
|
||||
contents.on('did-start-navigation', navigation)
|
||||
contents.on('before-input-event', beforeInput)
|
||||
window.on('closed', closed)
|
||||
window.on('blur', blur)
|
||||
disposers.add(dispose)
|
||||
},
|
||||
attach(window) { attachInput(window, window.webContents) },
|
||||
attachGuest: attachInput,
|
||||
dispose() {
|
||||
for (const dispose of disposers) dispose()
|
||||
persistence.dispose()
|
||||
|
||||
@@ -836,7 +836,7 @@ async function main(): Promise<void> {
|
||||
const createMainWindow = (): BrowserWindow => {
|
||||
const window = createWindow(appPreload, false, true)
|
||||
mainWindow = window
|
||||
browserGuests.bind(window)
|
||||
browserGuests.bind(window, (guest, name) => shortcuts.attachGuest(window, guest, name))
|
||||
shortcuts.attach(window)
|
||||
window.on('focus', automaticCheck)
|
||||
window.on('closed', () => { if (mainWindow === window) mainWindow = undefined })
|
||||
|
||||
@@ -24,6 +24,11 @@ function createProductApi(): DshDesktopProductApi {
|
||||
|| !element.matches('iframe[data-sidebar-browser-frame], iframe[data-html-preview]')) return
|
||||
if (input.frameName === '' || element.name !== input.frameName) return
|
||||
}
|
||||
if (input.kind === 'webview') {
|
||||
const element = document.activeElement
|
||||
if (element?.matches('webview[data-sidebar-browser-frame]') !== true || !element.isConnected
|
||||
|| input.frameName === '' || element.getAttribute('name') !== input.frameName) return
|
||||
}
|
||||
listener(input)
|
||||
}
|
||||
ipcRenderer.on(DESKTOP_IPC.shortcutsInput, handle)
|
||||
|
||||
@@ -4,17 +4,22 @@ import { mkdtemp, rm } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, expect, it, onTestFinished, vi } from 'vitest'
|
||||
import type { BrowserWindow, IpcMainInvokeEvent } from 'electron'
|
||||
import type { BrowserWindow, IpcMainInvokeEvent, WebContents } from 'electron'
|
||||
import type { DesktopShortcutInput, ShortcutBinding, ShortcutCommandId, ShortcutConfigSnapshot,
|
||||
ShortcutDefinition, ShortcutSaveResult } from '@deepseek-ai/dsh-client-shortcuts/protocol'
|
||||
import { ShortcutRegistry } from '../../../packages/client/shortcuts/src/client/registry.ts'
|
||||
import { installKeyboard } from '../../../packages/client/shortcuts/src/client/dom.ts'
|
||||
import { installNativeKeyboard } from '../../../packages/client/shortcuts/src/client/native.ts'
|
||||
import type { DesktopBrowserLeaseId } from '@deepseek-ai/dsh-client-ui-sidebar-browser/types'
|
||||
import { DESKTOP_IPC } from '../src/ipc.ts'
|
||||
|
||||
const ipc = vi.hoisted(() => ({ handle: vi.fn(), removeHandler: vi.fn() }))
|
||||
vi.mock('electron', () => ({ ipcMain: ipc }))
|
||||
vi.mock('electron', () => ({ ipcMain: ipc, app: { isPackaged: true }, session: { fromPartition: () => ({
|
||||
setPermissionRequestHandler: vi.fn(), setPermissionCheckHandler: vi.fn(), setDevicePermissionHandler: vi.fn(),
|
||||
setDisplayMediaRequestHandler: vi.fn(), on: vi.fn(), webRequest: { onBeforeRequest: vi.fn() },
|
||||
}) } }))
|
||||
const { installDesktopShortcuts } = await import('../src/keyboard.ts')
|
||||
const { DesktopBrowserGuests } = await import('../src/browser-guests.ts')
|
||||
afterEach(() => { vi.clearAllMocks() })
|
||||
|
||||
function desktopDefaults(binding: ShortcutBinding): ShortcutDefinition['defaults'] {
|
||||
@@ -523,3 +528,123 @@ it.each(['macos', 'windows'] as const)('requires fresh %s chord presses when Ele
|
||||
expect(f.contents.send).toHaveBeenCalledTimes(5)
|
||||
}
|
||||
})
|
||||
|
||||
it.each(['macos', 'windows', 'linux'] as const)('routes approved %s browser guest input to its owner and stops delivery before guest destruction', async (platform) => {
|
||||
const f = await fixture(platform)
|
||||
const snapshot = await f.call<ShortcutConfigSnapshot>(DESKTOP_IPC.shortcutsGet,
|
||||
[{ id: 'browser.new', defaults: desktopDefaults({ code: 'KeyT', modifiers: ['primary'] }) }])
|
||||
const guests = new DesktopBrowserGuests(() => undefined)
|
||||
const attach = vi.fn((guest: WebContents, name: DesktopBrowserLeaseId) => f.keyboard.attachGuest(f.window as never, guest, name))
|
||||
guests.bind(f.window as never, attach)
|
||||
const reservation = guests.acquire(f.contents as never, 'session:test')
|
||||
const frame = { url: `about:blank#${reservation.lease}`, parent: null }
|
||||
const guest = Object.assign(new EventEmitter(), { mainFrame: frame, focusedFrame: frame,
|
||||
getURL: () => frame.url, isDestroyed: () => false, isFocused: vi.fn(() => true),
|
||||
setWindowOpenHandler: vi.fn(), setIgnoreMenuShortcuts: vi.fn(), send: vi.fn(), close: vi.fn() })
|
||||
onTestFinished(() => { guest.emit('destroyed') })
|
||||
const rejected = { preventDefault: vi.fn() }
|
||||
f.contents.emit('will-attach-webview', rejected, {}, { src: 'about:blank#unknown', partition: reservation.partition })
|
||||
expect(rejected.preventDefault).toHaveBeenCalledOnce()
|
||||
const invalid = Object.assign(new EventEmitter(), { getURL: () => 'about:blank#unknown', setWindowOpenHandler: vi.fn(), close: vi.fn() })
|
||||
f.contents.emit('did-attach-webview', {}, invalid)
|
||||
invalid.emit('dom-ready')
|
||||
expect(invalid.close).toHaveBeenCalledExactlyOnceWith({ waitForBeforeUnload: false })
|
||||
expect(attach).not.toHaveBeenCalled()
|
||||
|
||||
const approved = { preventDefault: vi.fn() }
|
||||
f.contents.emit('will-attach-webview', approved, {}, { src: frame.url, partition: reservation.partition })
|
||||
expect(approved.preventDefault).not.toHaveBeenCalled()
|
||||
f.contents.emit('did-attach-webview', {}, guest)
|
||||
guest.emit('dom-ready')
|
||||
expect(attach).toHaveBeenCalledExactlyOnceWith(guest, reservation.lease)
|
||||
const input = { modifiers: [], type: 'keyDown', code: 'KeyT', key: 't', meta: platform === 'macos',
|
||||
control: platform !== 'macos', alt: false, shift: false, isAutoRepeat: false, isComposing: false }
|
||||
const preventDefault = vi.fn()
|
||||
f.contents.send.mockClear()
|
||||
guest.emit('before-input-event', { preventDefault }, input)
|
||||
expect(preventDefault).toHaveBeenCalledOnce()
|
||||
expect(guest.send).not.toHaveBeenCalled()
|
||||
expect(f.contents.send).toHaveBeenCalledExactlyOnceWith(DESKTOP_IPC.shortcutsInput, {
|
||||
kind: 'webview', frameName: reservation.lease, revision: snapshot.revision, code: 'KeyT',
|
||||
meta: platform === 'macos', control: platform !== 'macos', alt: false, shift: false, repeat: false,
|
||||
})
|
||||
guest.isFocused.mockReturnValue(false)
|
||||
guest.emit('before-input-event', { preventDefault }, input)
|
||||
expect(f.contents.send).toHaveBeenCalledOnce()
|
||||
guest.isFocused.mockReturnValue(true)
|
||||
const release = guests.release(f.contents as never, reservation.lease)
|
||||
expect(guest.close).toHaveBeenCalledExactlyOnceWith({ waitForBeforeUnload: false })
|
||||
expect(guest.listenerCount('before-input-event')).toBe(0)
|
||||
guest.emit('before-input-event', { preventDefault }, input)
|
||||
expect(f.contents.send).toHaveBeenCalledOnce()
|
||||
guest.emit('destroyed')
|
||||
await release
|
||||
})
|
||||
|
||||
it('keeps browser guest chord state local and leaves accepted bindings active through guest navigation', async () => {
|
||||
const f = await fixture('macos')
|
||||
const initial = await f.call<ShortcutConfigSnapshot>(DESKTOP_IPC.shortcutsGet, f.definitions)
|
||||
await f.call(DESKTOP_IPC.shortcutsEdit, { type: 'set', id: 'sidebar.left.toggle',
|
||||
binding: { code: 'KeyF', secondCode: 'KeyG', modifiers: [] } }, initial.revision)
|
||||
const frame = { url: 'https://example.test/', parent: null }
|
||||
const guest = Object.assign(new EventEmitter(), { mainFrame: frame, focusedFrame: frame,
|
||||
isDestroyed: () => false, isFocused: () => true, setIgnoreMenuShortcuts: vi.fn() })
|
||||
const blurListeners = f.window.listenerCount('blur')
|
||||
const closedListeners = f.window.listenerCount('closed')
|
||||
const dispose = f.keyboard.attachGuest(f.window as never, guest as never, 'guest' as DesktopBrowserLeaseId)
|
||||
onTestFinished(dispose)
|
||||
expect(f.window.listenerCount('blur')).toBe(blurListeners)
|
||||
expect(f.window.listenerCount('closed')).toBe(closedListeners)
|
||||
const press = (contents: EventEmitter, code: string): void => {
|
||||
contents.emit('before-input-event', { preventDefault: vi.fn() }, { modifiers: [], type: 'keyDown', code, key: code,
|
||||
meta: false, control: false, alt: false, shift: false, isAutoRepeat: false, isComposing: false })
|
||||
}
|
||||
f.contents.send.mockClear()
|
||||
press(guest, 'KeyF')
|
||||
guest.emit('blur')
|
||||
press(guest, 'KeyG')
|
||||
expect(f.contents.send).not.toHaveBeenCalled()
|
||||
f.window.emit('blur')
|
||||
press(guest, 'KeyF')
|
||||
expect(f.contents.send).not.toHaveBeenCalled()
|
||||
guest.emit('did-start-navigation', { isMainFrame: true, isSameDocument: false })
|
||||
press(guest, 'KeyF')
|
||||
press(f.contents, 'KeyG')
|
||||
expect(f.contents.send).not.toHaveBeenCalled()
|
||||
press(guest, 'KeyG')
|
||||
expect(f.contents.send).toHaveBeenCalledExactlyOnceWith(DESKTOP_IPC.shortcutsInput,
|
||||
expect.objectContaining({ kind: 'webview', frameName: 'guest', code: 'KeyF', secondCode: 'KeyG' }))
|
||||
guest.emit('destroyed')
|
||||
press(f.contents, 'KeyF')
|
||||
expect(f.contents.send).toHaveBeenCalledTimes(2)
|
||||
expect(f.contents.send).toHaveBeenLastCalledWith(DESKTOP_IPC.shortcutsInput,
|
||||
expect.objectContaining({ kind: 'keyboard', code: 'KeyF', secondCode: 'KeyG' }))
|
||||
})
|
||||
|
||||
it('delivers Windows Edit actions to the focused browser guest without invoking its custom shortcut', async () => {
|
||||
const f = await fixture('windows')
|
||||
const initial = await f.call<ShortcutConfigSnapshot>(DESKTOP_IPC.shortcutsGet, f.definitions)
|
||||
await f.call(DESKTOP_IPC.shortcutsEdit, { type: 'set', id: 'sidebar.left.toggle',
|
||||
binding: { code: 'KeyC', modifiers: ['control'] } }, initial.revision)
|
||||
const frame = { url: 'https://example.test/', parent: null }
|
||||
const guest = Object.assign(new EventEmitter(), { mainFrame: frame, focusedFrame: frame,
|
||||
isDestroyed: () => false, isFocused: () => true, setIgnoreMenuShortcuts: vi.fn(), focus: vi.fn(), sendInputEvent: vi.fn() })
|
||||
const dispose = f.keyboard.attachGuest(f.window as never, guest as never, 'guest' as DesktopBrowserLeaseId)
|
||||
onTestFinished(dispose)
|
||||
const events: string[] = []
|
||||
guest.sendInputEvent.mockImplementation((input: { type: 'keyDown' | 'keyUp' }) => {
|
||||
const preventDefault = vi.fn()
|
||||
guest.emit('before-input-event', { preventDefault }, { ...input, code: 'KeyC', key: 'c', modifiers: ['control'],
|
||||
control: true, meta: false, alt: false, shift: false, isAutoRepeat: false, isComposing: false })
|
||||
if (!preventDefault.mock.calls.length) events.push(input.type)
|
||||
})
|
||||
f.contents.send.mockClear()
|
||||
f.keyboard.sendEditingKey('C', ['control'])
|
||||
expect(guest.focus).toHaveBeenCalledOnce()
|
||||
expect(events).toEqual(['keyDown', 'keyUp'])
|
||||
expect(f.contents.send).not.toHaveBeenCalled()
|
||||
expect(f.contents.sendInputEvent).not.toHaveBeenCalled()
|
||||
dispose()
|
||||
f.keyboard.sendEditingKey('C', ['control'])
|
||||
expect(f.contents.sendInputEvent).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
@@ -189,3 +189,35 @@ it('forwards only the focused product iframe and releases native input subscript
|
||||
off()
|
||||
expect(electron.ipcRenderer.off).toHaveBeenCalledWith(DESKTOP_IPC.shortcutsInput, handler)
|
||||
})
|
||||
|
||||
it('forwards browser guest input only for the focused live webview lease', async () => {
|
||||
vi.stubGlobal('location', new URL('dsh-app://app/'))
|
||||
await import('../src/preload-app.ts')
|
||||
const api = electron.contextBridge.exposeInMainWorld.mock.calls.find(([name]) => name === 'dshDesktop')?.[1] as DshDesktopProductApi
|
||||
const listener = vi.fn()
|
||||
const off = api.keyboard.subscribe(listener)
|
||||
const handler = electron.ipcRenderer.on.mock.calls.find(([name]) => name === DESKTOP_IPC.shortcutsInput)![1] as
|
||||
(event: unknown, state: unknown) => void
|
||||
const input = { kind: 'webview', frameName: 'guest', revision: 'current' }
|
||||
const frame = document.createElement('webview')
|
||||
frame.tabIndex = 0
|
||||
frame.setAttribute('name', 'guest')
|
||||
document.body.append(frame)
|
||||
frame.focus()
|
||||
handler({}, input)
|
||||
expect(listener).not.toHaveBeenCalled()
|
||||
frame.setAttribute('data-sidebar-browser-frame', 'webview')
|
||||
handler({}, { ...input, frameName: '' })
|
||||
handler({}, { ...input, frameName: 'old' })
|
||||
expect(listener).not.toHaveBeenCalled()
|
||||
handler({}, input)
|
||||
expect(listener).toHaveBeenCalledExactlyOnceWith(input)
|
||||
const other = document.createElement('input')
|
||||
document.body.append(other)
|
||||
other.focus()
|
||||
handler({}, input)
|
||||
frame.remove()
|
||||
handler({}, input)
|
||||
expect(listener).toHaveBeenCalledOnce()
|
||||
off()
|
||||
})
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write docs/config-catalog.md
|
||||
config-catalog.md: 573d16f37b9ce3c123c55c366d8ee6424feb3d64
|
||||
config-catalog.zh.md: 9d827a4c65ec198239d7d1ef29735747a39838b9
|
||||
config-catalog.md: 5957a8be6abaa5906cf358a2a1dbaac0cb8d1c39
|
||||
config-catalog.zh.md: 7058b149bb9aa9236764c754dde7622db4090901
|
||||
@@ -481,6 +481,20 @@ export interface Config {
|
||||
|
||||
Source: [`packages/client/hmr/src/index.ts:30`](../packages/client/hmr/src/index.ts)
|
||||
|
||||
<a id="deepseek-aidsh-client-shortcuts"></a>
|
||||
|
||||
## `@deepseek-ai/dsh-client-shortcuts`
|
||||
|
||||
```ts config-catalog
|
||||
/** Fixed shortcut sequence settings. */
|
||||
export interface Config {
|
||||
/** Maximum interval between independent Escape presses for stopping a reply, in milliseconds. */
|
||||
stopSequenceMs: number
|
||||
}
|
||||
```
|
||||
|
||||
Source: [`packages/client/shortcuts/src/config.ts:5`](../packages/client/shortcuts/src/config.ts)
|
||||
|
||||
<a id="deepseek-aidsh-client-ui-settings-account"></a>
|
||||
|
||||
## `@deepseek-ai/dsh-client-ui-settings-account`
|
||||
@@ -4127,6 +4141,7 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co
|
||||
- `@deepseek-ai/dsh-client-ui-settings-shell` ([`packages/client/ui-settings-shell/src/index.ts`](../packages/client/ui-settings-shell/src/index.ts))
|
||||
- `@deepseek-ai/dsh-client-ui-settings-subagent` ([`packages/client/ui-settings-subagent/src/index.ts`](../packages/client/ui-settings-subagent/src/index.ts))
|
||||
- `@deepseek-ai/dsh-client-ui-settings-web-search` ([`packages/client/ui-settings-web-search/src/index.ts`](../packages/client/ui-settings-web-search/src/index.ts))
|
||||
- `@deepseek-ai/dsh-client-ui-shortcuts` ([`packages/client/ui-shortcuts/src/index.ts`](../packages/client/ui-shortcuts/src/index.ts))
|
||||
- `@deepseek-ai/dsh-client-ui-sidebar` ([`packages/client/ui-sidebar/src/index.ts`](../packages/client/ui-sidebar/src/index.ts))
|
||||
- `@deepseek-ai/dsh-client-ui-sidebar-browser` ([`packages/client/ui-sidebar-browser/src/index.ts`](../packages/client/ui-sidebar-browser/src/index.ts))
|
||||
- `@deepseek-ai/dsh-client-ui-sidebar-files` ([`packages/client/ui-sidebar-files/src/index.ts`](../packages/client/ui-sidebar-files/src/index.ts))
|
||||
|
||||
@@ -483,6 +483,20 @@ export interface Config {
|
||||
|
||||
来源: [`packages/client/hmr/src/index.ts:30`](../packages/client/hmr/src/index.ts)
|
||||
|
||||
<a id="deepseek-aidsh-client-shortcuts"></a>
|
||||
|
||||
## `@deepseek-ai/dsh-client-shortcuts`
|
||||
|
||||
```ts config-catalog
|
||||
/** Fixed shortcut sequence settings. */
|
||||
export interface Config {
|
||||
/** Maximum interval between independent Escape presses for stopping a reply, in milliseconds. */
|
||||
stopSequenceMs: number
|
||||
}
|
||||
```
|
||||
|
||||
来源: [`packages/client/shortcuts/src/config.ts:5`](../packages/client/shortcuts/src/config.ts)
|
||||
|
||||
<a id="deepseek-aidsh-client-ui-settings-account"></a>
|
||||
|
||||
## `@deepseek-ai/dsh-client-ui-settings-account`
|
||||
@@ -4129,6 +4143,7 @@ export interface Config {
|
||||
- `@deepseek-ai/dsh-client-ui-settings-shell`([`packages/client/ui-settings-shell/src/index.ts`](../packages/client/ui-settings-shell/src/index.ts))
|
||||
- `@deepseek-ai/dsh-client-ui-settings-subagent`([`packages/client/ui-settings-subagent/src/index.ts`](../packages/client/ui-settings-subagent/src/index.ts))
|
||||
- `@deepseek-ai/dsh-client-ui-settings-web-search`([`packages/client/ui-settings-web-search/src/index.ts`](../packages/client/ui-settings-web-search/src/index.ts))
|
||||
- `@deepseek-ai/dsh-client-ui-shortcuts` ([`packages/client/ui-shortcuts/src/index.ts`](../packages/client/ui-shortcuts/src/index.ts))
|
||||
- `@deepseek-ai/dsh-client-ui-sidebar`([`packages/client/ui-sidebar/src/index.ts`](../packages/client/ui-sidebar/src/index.ts))
|
||||
- `@deepseek-ai/dsh-client-ui-sidebar-browser`([`packages/client/ui-sidebar-browser/src/index.ts`](../packages/client/ui-sidebar-browser/src/index.ts))
|
||||
- `@deepseek-ai/dsh-client-ui-sidebar-files`([`packages/client/ui-sidebar-files/src/index.ts`](../packages/client/ui-sidebar-files/src/index.ts))
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write docs/event-producer-consumer.md
|
||||
event-producer-consumer.md: 13ac82b2c9fb0b2aeeaa2bec9c736db5b5b63992
|
||||
event-producer-consumer.zh.md: fce8463041181139e45fb5f3a9ab14c87117135f
|
||||
event-producer-consumer.md: bb80e1a51bb5e718d2bef7fdf24b3a0048645df2
|
||||
event-producer-consumer.zh.md: ccdf22451656aa80493dc060170ca6e5e3f4b46f
|
||||
@@ -75,7 +75,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac
|
||||
| `tools/ptc-dispatch-log` | `waterfall` | [`packages/core/tools/src/index.ts:190`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`spill-policy`](../packages/spill/spill-policy) |
|
||||
| `tools/result` | `emit` | [`packages/core/tools/src/index.ts:198`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`events.dispatch`) | [`agent-instructions`](../packages/context/agent-instructions), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver), [`tool-present`](../packages/deliverables/tool-present) |
|
||||
| `user-questions/request` | `waterfall` | [`packages/interaction/user-questions/src/types.ts:88`](../packages/interaction/user-questions/src/types.ts) | [`user-questions`](../packages/interaction/user-questions) (`waterfall`) | `remotes` |
|
||||
| `webserver/index-inject` | `emit` | [`packages/host/webserver/src/index.ts:34`](../packages/host/webserver/src/index.ts) | `webserver` (`emit`) | `connection`, `inspector`, `modules`, `ui-sidebar-documentpreview`, `ui-theme` |
|
||||
| `webserver/index-inject` | `emit` | [`packages/host/webserver/src/index.ts:34`](../packages/host/webserver/src/index.ts) | `webserver` (`emit`) | `connection`, `inspector`, `modules`, `shortcuts`, `ui-sidebar-documentpreview`, `ui-theme` |
|
||||
| `workflow/agent-end` | `emit` | [`packages/workflow/workflow/src/index.ts:79`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | [`tool-workflow`](../packages/workflow/tool-workflow), [`workflow`](../packages/workflow/workflow) |
|
||||
| `workflow/agent-start` | `emit` | [`packages/workflow/workflow/src/index.ts:68`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | [`tool-workflow`](../packages/workflow/tool-workflow), [`workflow`](../packages/workflow/workflow) |
|
||||
| `workflow/end` | `emit` | [`packages/workflow/workflow/src/index.ts:89`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | [`workflow`](../packages/workflow/workflow) |
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
| `tools/ptc-dispatch-log` | `waterfall` | [`packages/core/tools/src/index.ts:190`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`spill-policy`](../packages/spill/spill-policy) |
|
||||
| `tools/result` | `emit` | [`packages/core/tools/src/index.ts:198`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`events.dispatch`) | [`agent-instructions`](../packages/context/agent-instructions), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver), [`tool-present`](../packages/deliverables/tool-present) |
|
||||
| `user-questions/request` | `waterfall` | [`packages/interaction/user-questions/src/types.ts:88`](../packages/interaction/user-questions/src/types.ts) | [`user-questions`](../packages/interaction/user-questions) (`waterfall`) | `remotes` |
|
||||
| `webserver/index-inject` | `emit` | [`packages/host/webserver/src/index.ts:34`](../packages/host/webserver/src/index.ts) | `webserver` (`emit`) | `connection`, `inspector`, `modules`, `ui-sidebar-documentpreview`, `ui-theme` |
|
||||
| `webserver/index-inject` | `emit` | [`packages/host/webserver/src/index.ts:34`](../packages/host/webserver/src/index.ts) | `webserver` (`emit`) | `connection`, `inspector`, `modules`, `shortcuts`, `ui-sidebar-documentpreview`, `ui-theme` |
|
||||
| `workflow/agent-end` | `emit` | [`packages/workflow/workflow/src/index.ts:79`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | [`tool-workflow`](../packages/workflow/tool-workflow), [`workflow`](../packages/workflow/workflow) |
|
||||
| `workflow/agent-start` | `emit` | [`packages/workflow/workflow/src/index.ts:68`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | [`tool-workflow`](../packages/workflow/tool-workflow), [`workflow`](../packages/workflow/workflow) |
|
||||
| `workflow/end` | `emit` | [`packages/workflow/workflow/src/index.ts:89`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | [`workflow`](../packages/workflow/workflow) |
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write docs/module-graph.md
|
||||
module-graph.md: fc5c06727e58705ac6d9b44470b1b37010dc3223
|
||||
module-graph.zh.md: 6911d9820932569c7a666b47a8bd53696fdb6393
|
||||
module-graph.md: 5e07e6a4a38cbcd866dcbaab1cbf449e3e822c09
|
||||
module-graph.zh.md: 9d094d061872000e840be1c09e9ea6e458222318
|
||||
@@ -152,6 +152,7 @@ flowchart TD
|
||||
pkg_client_locale["client-locale"]
|
||||
pkg_client_modules["client-modules"]
|
||||
pkg_client_resources["client-resources"]
|
||||
pkg_client_shortcuts["client-shortcuts"]
|
||||
pkg_client_store["client-store"]
|
||||
pkg_client_ui_agent_preset["client-ui-agent-preset"]
|
||||
pkg_client_ui_approval["client-ui-approval"]
|
||||
@@ -189,6 +190,7 @@ flowchart TD
|
||||
pkg_client_ui_settings_shell["client-ui-settings-shell"]
|
||||
pkg_client_ui_settings_subagent["client-ui-settings-subagent"]
|
||||
pkg_client_ui_settings_web_search["client-ui-settings-web-search"]
|
||||
pkg_client_ui_shortcuts["client-ui-shortcuts"]
|
||||
pkg_client_ui_sidebar["client-ui-sidebar"]
|
||||
pkg_client_ui_sidebar_browser["client-ui-sidebar-browser"]
|
||||
pkg_client_ui_sidebar_documentpreview["client-ui-sidebar-documentpreview"]
|
||||
@@ -1372,6 +1374,7 @@ flowchart TD
|
||||
| [`client-locale`](../packages/client/locale) | `client` | — |
|
||||
| [`client-modules`](../packages/client/modules) | `client` | — |
|
||||
| [`client-resources`](../packages/client/resources) | `client` | — |
|
||||
| [`client-shortcuts`](../packages/client/shortcuts) | `client` | — |
|
||||
| [`client-store`](../packages/client/store) | `client` | — |
|
||||
| [`client-ui-agent-preset`](../packages/client/ui-agent-preset) | `client` | — |
|
||||
| [`client-ui-approval`](../packages/client/ui-approval) | `client` | — |
|
||||
@@ -1409,6 +1412,7 @@ flowchart TD
|
||||
| [`client-ui-settings-shell`](../packages/client/ui-settings-shell) | `client` | — |
|
||||
| [`client-ui-settings-subagent`](../packages/client/ui-settings-subagent) | `client` | — |
|
||||
| [`client-ui-settings-web-search`](../packages/client/ui-settings-web-search) | `client` | — |
|
||||
| [`client-ui-shortcuts`](../packages/client/ui-shortcuts) | `client` | — |
|
||||
| [`client-ui-sidebar`](../packages/client/ui-sidebar) | `client` | — |
|
||||
| [`client-ui-sidebar-browser`](../packages/client/ui-sidebar-browser) | `client` | — |
|
||||
| [`client-ui-sidebar-documentpreview`](../packages/client/ui-sidebar-documentpreview) | `client` | — |
|
||||
|
||||
@@ -154,6 +154,7 @@ flowchart TD
|
||||
pkg_client_locale["client-locale"]
|
||||
pkg_client_modules["client-modules"]
|
||||
pkg_client_resources["client-resources"]
|
||||
pkg_client_shortcuts["client-shortcuts"]
|
||||
pkg_client_store["client-store"]
|
||||
pkg_client_ui_agent_preset["client-ui-agent-preset"]
|
||||
pkg_client_ui_approval["client-ui-approval"]
|
||||
@@ -191,6 +192,7 @@ flowchart TD
|
||||
pkg_client_ui_settings_shell["client-ui-settings-shell"]
|
||||
pkg_client_ui_settings_subagent["client-ui-settings-subagent"]
|
||||
pkg_client_ui_settings_web_search["client-ui-settings-web-search"]
|
||||
pkg_client_ui_shortcuts["client-ui-shortcuts"]
|
||||
pkg_client_ui_sidebar["client-ui-sidebar"]
|
||||
pkg_client_ui_sidebar_browser["client-ui-sidebar-browser"]
|
||||
pkg_client_ui_sidebar_documentpreview["client-ui-sidebar-documentpreview"]
|
||||
@@ -1374,6 +1376,7 @@ flowchart TD
|
||||
| [`client-locale`](../packages/client/locale) | `client` | — |
|
||||
| [`client-modules`](../packages/client/modules) | `client` | — |
|
||||
| [`client-resources`](../packages/client/resources) | `client` | — |
|
||||
| [`client-shortcuts`](../packages/client/shortcuts) | `client` | — |
|
||||
| [`client-store`](../packages/client/store) | `client` | — |
|
||||
| [`client-ui-agent-preset`](../packages/client/ui-agent-preset) | `client` | — |
|
||||
| [`client-ui-approval`](../packages/client/ui-approval) | `client` | — |
|
||||
@@ -1411,6 +1414,7 @@ flowchart TD
|
||||
| [`client-ui-settings-shell`](../packages/client/ui-settings-shell) | `client` | — |
|
||||
| [`client-ui-settings-subagent`](../packages/client/ui-settings-subagent) | `client` | — |
|
||||
| [`client-ui-settings-web-search`](../packages/client/ui-settings-web-search) | `client` | — |
|
||||
| [`client-ui-shortcuts`](../packages/client/ui-shortcuts) | `client` | — |
|
||||
| [`client-ui-sidebar`](../packages/client/ui-sidebar) | `client` | — |
|
||||
| [`client-ui-sidebar-browser`](../packages/client/ui-sidebar-browser) | `client` | — |
|
||||
| [`client-ui-sidebar-documentpreview`](../packages/client/ui-sidebar-documentpreview) | `client` | — |
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/client/README.md
|
||||
README.md: 8a5f789a4ee6343e00ee2b9cdc68d6bf7e7f8d79
|
||||
README.zh.md: ae5dbb237f504ca0cd66a8105dd772493b356326
|
||||
README.md: a7e4228fac5e4e9c7b50a67b8d66cb244df945f7
|
||||
README.zh.md: d5c33dc99d40e1e203848aa14a8c23c492b011a8
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/client/shortcuts/README.md
|
||||
README.md: 7cfba95eaceb262b20870b521236fd6d7fe639a9
|
||||
README.zh.md: 84960f311c9eda2ea0e0fa62e38196b427f91a2f
|
||||
README.md: 3583a31046946a29d3f8ddb1b263d4b676d45dc0
|
||||
README.zh.md: 0f1fef10405509740b2e0ce1f672946c1d898abb
|
||||
@@ -49,7 +49,7 @@ Windows and macOS Desktop intercept complete accepted combinations before local
|
||||
|
||||
DOM fixed-input observers receive composition and consumption flags before application dispatch. Consuming an event makes it unavailable to subsequent observers and commands. Focus, pointer, window blur, composition, modal changes, and locally stopped propagation reset pending DOM sequences. Native chords reset on window blur, focused-frame or configuration changes, composition, recording, and modifier release. Electron can omit keyups after native interception; completing a native binding discards held-key state, so each chord activation needs two fresh presses. Releasing a modifier clears held-key state because macOS may omit character keyup events while Command is held. The adapter removes its listeners and modal observer when disposed; one feature's observer failure does not prevent the remaining observers from receiving input.
|
||||
|
||||
Desktop native menus and embedded frames use the same command registry as DOM input. Preload matches the focused iframe name against the embedding frame reported by Electron; the client rejects messages from an older configuration. Native menu selection is explicit and works without a bound accelerator. Two-key chords have no native menu accelerator; their labels remain visible in the shortcut reference. Terminal Ctrl+W/R remains local input outside Windows and macOS Desktop. Explicit native-menu actions retain their modal checks; keyboard bindings use the device dispatch policy.
|
||||
Desktop native menus and embedded frames use the same command registry as DOM input. Preload and the client match the focused iframe name or browser webview lease against native input; the client rejects messages from an older configuration. Browser guest input ends when its lease is released, before guest destruction finishes. Native menu selection is explicit and works without a bound accelerator. Two-key chords have no native menu accelerator; their labels remain visible in the shortcut reference. Terminal Ctrl+W/R remains local input outside Windows and macOS Desktop. Explicit native-menu actions retain their modal checks; keyboard bindings use the device dispatch policy.
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ Windows 和 macOS Desktop 在局部处理器之前拦截已接受的完整组合
|
||||
|
||||
DOM 固定输入观察者在应用分发前收到输入法组合和消费标记。消费事件后,后续观察者和命令不能使用它。焦点、指针、窗口失焦、输入法组合、模态变化和局部停止传播会重置待完成的 DOM 序列。原生双键状态在窗口失焦、焦点 frame 或配置变化、输入法组合、录制和修饰键释放时重置。Electron 在原生拦截后可能省略 keyup;原生绑定完成时立即清空按住状态,因此每次触发双键组合都需要重新按下两个键。释放修饰键会清空按住状态,因为 macOS 在按住 Command 时可能省略字符键的 keyup。适配器释放时移除监听器和模态观察者;单个功能观察者失败不妨碍其他观察者接收输入。
|
||||
|
||||
Desktop 原生菜单和内嵌 frame 使用与 DOM 输入相同的命令注册表。Preload 将获得焦点的 iframe 名称与 Electron 报告的嵌入 frame 匹配;Client 拒绝旧配置消息。原生菜单选择是显式操作,不依赖快捷键绑定。双键组合没有原生菜单快捷键,其键位仍显示在快捷键速查中。Windows 和 macOS Desktop 之外的终端 Ctrl+W/R 保留为局部输入。显式原生菜单操作保留模态检查;键盘绑定使用对应设备的分发策略。
|
||||
Desktop 原生菜单和内嵌 frame 使用与 DOM 输入相同的命令注册表。Preload 和 Client 将获焦 iframe 的名称或浏览器 webview 租约与原生输入匹配;Client 拒绝旧配置消息。浏览器 guest 的输入监听在租约释放时结束,无需等待 guest 销毁完成。原生菜单选择是显式操作,不依赖快捷键绑定。双键组合没有原生菜单快捷键,其键位仍显示在快捷键速查中。Windows 和 macOS Desktop 之外的终端 Ctrl+W/R 保留为局部输入。显式原生菜单操作保留模态检查;键盘绑定使用对应设备的分发策略。
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/** Desktop gestures use live focus, modal state, and verified iframe ownership. */
|
||||
/** Desktop gestures use live focus, modal state, and verified embedding ownership. */
|
||||
import type { DesktopKeyboardApi, ShortcutConfigSnapshot } from '../protocol.ts'
|
||||
import type { ShortcutRegistry } from './registry.ts'
|
||||
|
||||
@@ -17,7 +17,9 @@ export function installNativeKeyboard(window: Window, keyboard: DesktopKeyboardA
|
||||
if (input.revision !== snapshot().revision) return
|
||||
reset?.()
|
||||
let target = window.document.activeElement
|
||||
while (target?.shadowRoot?.activeElement != null) target = target.shadowRoot.activeElement
|
||||
while (target?.shadowRoot?.activeElement != null && !target.matches('webview[data-sidebar-browser-frame]')) {
|
||||
target = target.shadowRoot.activeElement
|
||||
}
|
||||
const top = [...window.document.querySelectorAll<HTMLElement>('[role="dialog"][aria-modal="true"], [role="menu"]')].at(-1)
|
||||
const region = target?.closest('.xterm') ? 'terminal' as const
|
||||
: target?.matches('input, textarea, select, [contenteditable="true"], [contenteditable=""]') ? 'editable' as const : 'page' as const
|
||||
@@ -28,6 +30,12 @@ export function installNativeKeyboard(window: Window, keyboard: DesktopKeyboardA
|
||||
return
|
||||
}
|
||||
// Renderer focus and embedding identity may change after preload verification.
|
||||
if (input.kind === 'webview') {
|
||||
if (target?.matches('webview[data-sidebar-browser-frame]') !== true || !target.isConnected
|
||||
|| input.frameName === '' || target.getAttribute('name') !== input.frameName) return
|
||||
registry.dispatch({ ...input, composing: false, defaultPrevented: false }, { ...context, source: 'webview' }, () => {})
|
||||
return
|
||||
}
|
||||
if (!(target instanceof HTMLIFrameElement) || !target.isConnected
|
||||
|| !target.matches('iframe[data-sidebar-browser-frame], iframe[data-html-preview]')
|
||||
|| input.frameName === '' || target.name !== input.frameName) return
|
||||
|
||||
@@ -6,7 +6,7 @@ import type { BindingIssue, NormalizedBinding, ShortcutConfigSnapshot, ShortcutE
|
||||
export type ShortcutRegion = 'page' | 'editable' | 'terminal'
|
||||
/** Synchronous DOM context; owners capture business targets in resolve(). */
|
||||
export interface ShortcutContext {
|
||||
readonly source?: 'keyboard' | 'menu' | 'iframe'
|
||||
readonly source?: 'keyboard' | 'menu' | 'iframe' | 'webview'
|
||||
readonly region: ShortcutRegion
|
||||
readonly modal: string | null
|
||||
/** Original input element, or null when no document target is available. */
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
import type { ShortcutRevision } from './persistence.ts'
|
||||
import type { ShortcutCommandId } from './binding.ts'
|
||||
|
||||
/** Native input carries the accepted configuration identity; iframe names identify the focused embedding element. */
|
||||
/** Native input carries the accepted configuration identity; frame names identify the focused iframe or browser guest embedding element. */
|
||||
export type DesktopShortcutInput = {
|
||||
readonly revision: ShortcutRevision
|
||||
} & ({ readonly kind: 'menu'; readonly commandId: ShortcutCommandId }
|
||||
| { readonly kind: 'keyboard' | 'iframe'
|
||||
| { readonly kind: 'keyboard' | 'iframe' | 'webview'
|
||||
readonly frameName: string
|
||||
readonly code: string
|
||||
readonly secondCode?: string
|
||||
|
||||
@@ -125,3 +125,43 @@ it.each(['macos', 'windows'] as const)('runs a custom %s chord across text field
|
||||
expect(run).toHaveBeenCalledTimes(2)
|
||||
} finally { off() }
|
||||
})
|
||||
|
||||
it('checks the focused browser guest lease before dispatch and retains its embedding context across shadow focus', () => {
|
||||
const registry = new ShortcutRegistry('desktop', 'macos')
|
||||
const run = vi.fn()
|
||||
const resolve = vi.fn(() => ({ status: 'handled' as const, run }))
|
||||
registry.register({ id: 'browser.new' as ShortcutCommandId, label: () => 'Browser', aliases: [],
|
||||
defaults: { 'desktop:macos': { code: 'KeyT', modifiers: ['primary'] } }, regions: ['page'], modals: [], resolve })
|
||||
let deliver: (input: DesktopShortcutInput) => void = () => {}
|
||||
const off = installNativeKeyboard(window, { subscribe: (listener) => { deliver = listener; return () => {} }, closeWindow: vi.fn() },
|
||||
registry, () => registry.config.getSnapshot())
|
||||
const input: DesktopShortcutInput = { kind: 'webview', frameName: 'guest', revision: registry.config.getSnapshot().revision,
|
||||
code: 'KeyT', control: false, alt: false, shift: false, meta: true, repeat: false }
|
||||
const frame = document.createElement('webview')
|
||||
frame.tabIndex = 0
|
||||
frame.setAttribute('name', 'guest')
|
||||
document.body.append(frame)
|
||||
frame.focus()
|
||||
const shadow = document.createElement('div').attachShadow({ mode: 'open' })
|
||||
Object.defineProperty(shadow, 'activeElement', { value: document.createElement('input') })
|
||||
const shadowRoot = vi.spyOn(frame, 'shadowRoot', 'get').mockReturnValue(shadow)
|
||||
try {
|
||||
deliver(input)
|
||||
expect(run).not.toHaveBeenCalled()
|
||||
frame.setAttribute('data-sidebar-browser-frame', 'webview')
|
||||
deliver({ ...input, frameName: '' })
|
||||
deliver({ ...input, frameName: 'old' })
|
||||
deliver({ ...input, revision: 'old' as typeof input.revision })
|
||||
expect(run).not.toHaveBeenCalled()
|
||||
deliver(input)
|
||||
expect(run).toHaveBeenCalledOnce()
|
||||
expect(resolve).toHaveBeenCalledWith(expect.objectContaining({ source: 'webview', target: frame }))
|
||||
deliver({ ...input, repeat: true })
|
||||
frame.remove()
|
||||
deliver(input)
|
||||
expect(run).toHaveBeenCalledOnce()
|
||||
} finally {
|
||||
shadowRoot.mockRestore()
|
||||
off()
|
||||
}
|
||||
})
|
||||
@@ -16,7 +16,7 @@
|
||||
"path": "../../util/brand"
|
||||
},
|
||||
{
|
||||
"path": "../locale"
|
||||
"path": "../locale/tsconfig.client.json"
|
||||
},
|
||||
{
|
||||
"path": "../store"
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/client/ui-conversation/README.md
|
||||
README.md: 1cba7ffd9ec211b34f822afd3032263b2879b8e1
|
||||
README.zh.md: 350634cc2c7510af1203fe3eed286f707aa398ba
|
||||
README.md: cef9e7cfa0498d1b8eb9d81bb7596f385621e39c
|
||||
README.zh.md: 4365739aa5e9d18ef538fd349d9b6dd1c32ed130
|
||||
@@ -23,7 +23,7 @@ export type OpenTargetOperation = { readonly kind: 'default' } | { readonly kind
|
||||
|
||||
/** Inputs shared by both target adapters and the file empty-state action. */
|
||||
export interface OpenTargetButtonProps {
|
||||
readonly shortcut?: Pick<ShortcutCatalogEntry, 'keys' | 'aria'>
|
||||
readonly shortcut?: Pick<ShortcutCatalogEntry, 'keys' | 'aria'> | undefined
|
||||
readonly kind: 'file' | 'directory'
|
||||
readonly applications: readonly OpenTargetApplication[]
|
||||
readonly defaultId: string | undefined
|
||||
|
||||
@@ -51,7 +51,7 @@ async function bench() {
|
||||
'deliverables.review.file.actions': { kind: 'list', scope: 'session' },
|
||||
},
|
||||
} as never, () => null)
|
||||
const list = createSnapshotStore<SessionListState>({ ids: [], byId: {}, phase: 'ready', subagentsByParent: {}, jobsBySession: {} })
|
||||
const list = createSnapshotStore<SessionListState>({ ids: [], byId: {}, phase: 'ready', projectionsBySession: {} })
|
||||
const commands = new Map<string, ShortcutCommand>()
|
||||
const layoutStore = createLayoutStore().create()
|
||||
const layout = new LayoutController(layoutStore.actions, id => id === 'plugins', {
|
||||
|
||||
@@ -81,6 +81,8 @@ describe('ui-plan browser apply', () => {
|
||||
store.actions.open(parent)
|
||||
const unbind = controller.bind({
|
||||
sessionId: parent, actions: store.actions, surfaces: store.getSnapshot().bySession, canSplitPane: () => true,
|
||||
closeWithFocus: (_paneId, close) => { close() },
|
||||
openWithFocus: (open) => { open() },
|
||||
})
|
||||
b.openResource.mockImplementation(controller.openResource.bind(controller))
|
||||
b.openResourceIn.mockImplementation(controller.openResourceIn.bind(controller))
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
"path": "../shortcuts/tsconfig.client.json"
|
||||
},
|
||||
{
|
||||
"path": "../locale"
|
||||
"path": "../locale/tsconfig.client.json"
|
||||
},
|
||||
{
|
||||
"path": "../store"
|
||||
@@ -36,7 +36,7 @@
|
||||
"path": "../ui-layout"
|
||||
},
|
||||
{
|
||||
"path": "../ui-settings"
|
||||
"path": "../ui-settings/tsconfig.client.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/client/ui-sidebar-browser/README.md
|
||||
README.md: 33ef2d8e4dd82a67f826b4b446604ac88d3458ea
|
||||
README.zh.md: 1d822796ed33cc6ac27c6d75e27a7d44039c7711
|
||||
README.md: 46d08fd74e4f4605812c8d6e014a9fc887e76def
|
||||
README.zh.md: b8cd0c3213fbf1514d1d9d5c97bf7d6f0e865099
|
||||
@@ -70,7 +70,7 @@ Each tab's `BrowserController` owns address validation, commands and explicit re
|
||||
|
||||
Desktop's main process approves guest leases and enforces attachment, navigation and permission policy. Preload exposes only scoped Browser operations. Shared declarations use the standard `/types` export with `import type`; the Host and Client compile through separate tsconfig files. Desktop Browser tabs declare `keepMounted`, so Sidebar preserves their DOM across tab changes, Session switches, collapse and floating.
|
||||
|
||||
The page refresh shortcut calls the same reload operation as the toolbar. Its tooltip and ARIA key combination follow the effective binding. Desktop can route that shortcut from the embedded page; Web leaves browser-reserved combinations unchanged.
|
||||
The page refresh shortcut calls the same reload operation as the toolbar. Its tooltip and ARIA key combination follow the effective binding. Desktop routes accepted shortcuts from an approved guest through its owning window; the focused webview must still carry that guest’s lease. Web leaves browser-reserved combinations unchanged.
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ Web 记录 toolbar 提交和 typed tab 打开。导航状态机把每个受控 r
|
||||
|
||||
Desktop 主进程批准 guest 租约,并执行挂载、导航和权限策略。preload 只暴露限定范围的 Browser 操作。共享声明通过标准 `/types` 出口配合 `import type` 引入;Host 与 Client 使用独立 tsconfig 编译。Desktop Browser tab 声明 `keepMounted`,Sidebar 因而在切 tab、切 Session、收起与浮动期间保留其 DOM。
|
||||
|
||||
页面刷新快捷键调用工具栏使用的同一重载操作。其 Tooltip 和 ARIA 组合随有效绑定更新。桌面可从内嵌页面路由该快捷键;Web 保留浏览器专用组合。
|
||||
页面刷新快捷键调用工具栏使用的同一重载操作。其 Tooltip 和 ARIA 组合随有效绑定更新。Desktop 通过所属窗口路由已批准 guest 中的有效快捷键;获焦 webview 必须仍持有该 guest 的租约。Web 保留浏览器专用组合。
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
@@ -53,6 +53,7 @@ export class ElectronWebviewPresentation implements BrowserPresentation {
|
||||
const element = document.createElement('webview') as WebviewElement
|
||||
element.className = css.webview as string
|
||||
element.dataset.sidebarBrowserFrame = 'webview'
|
||||
element.setAttribute('name', reservation.lease)
|
||||
element.setAttribute('partition', reservation.partition)
|
||||
element.setAttribute('allowpopups', '')
|
||||
element.setAttribute('src', 'about:blank#' + reservation.lease)
|
||||
|
||||
@@ -24,6 +24,7 @@ it('waits for a mounted container and observes native history, titles and new-ta
|
||||
expect(h.bridge.acquire).not.toHaveBeenCalled()
|
||||
h.mount()
|
||||
const guest = await h.guest()
|
||||
expect(guest.element.getAttribute('name')).toBe(h.reservation.lease)
|
||||
expect(guest.element.getAttribute('partition')).toBe(h.reservation.partition)
|
||||
expect(guest.element.getAttribute('src')).toBe(`about:blank#${h.reservation.lease}`)
|
||||
guest.emit('dom-ready')
|
||||
|
||||
@@ -171,7 +171,10 @@ export interface SidebarRightTabInfo {
|
||||
readonly tab: TabRecord & {
|
||||
/** Effective refresh binding for this page's controls. */
|
||||
readonly refreshShortcut?: ShortcutCatalogEntry | undefined
|
||||
/** Only the foreground Session is visible. Docked bodies require expansion and selection; expanded titles include inactive tabs. Floats survive collapse. */
|
||||
/**
|
||||
* Only the foreground Session is visible. Docked bodies require expansion and selection;
|
||||
* expanded titles include inactive tabs. Floats survive collapse.
|
||||
*/
|
||||
readonly visible: boolean
|
||||
readonly navigation: SidebarRightTabNavigation
|
||||
/** Aborted only when the record disappears or this plugin unloads, not on hide or session switch. */
|
||||
|
||||
@@ -8,7 +8,9 @@ import type { ForkSessionInjected, SessionMenuItemProps } from '../contract/slot
|
||||
* @param props - owner share, menu open state, and the fork share.
|
||||
* @returns the row.
|
||||
*/
|
||||
export function ForkSessionMenuItem({ sessionId, useMenuOpenState, useShortcuts, forkSession, t }: SessionMenuItemProps<ForkSessionInjected>) {
|
||||
export function ForkSessionMenuItem({
|
||||
sessionId, useMenuOpenState, useShortcuts, forkSession, t,
|
||||
}: SessionMenuItemProps<ForkSessionInjected>) {
|
||||
const [, setMenuOpen] = useMenuOpenState()
|
||||
const shortcut = useShortcuts(rows => rows.find(row => row.id === 'session.fork'))
|
||||
return (
|
||||
|
||||
@@ -48,9 +48,13 @@ async function bench(runtime: 'web' | 'desktop' = 'desktop') {
|
||||
locale.register('workspace', { en, zh })
|
||||
locale.setLocale('en')
|
||||
ctx.provide('locale', locale)
|
||||
const navigation = { startSession: vi.fn(), forkSession: vi.fn(async () => {}), archiveSession: vi.fn(async () => {}) }
|
||||
const navigation = {
|
||||
startSession: vi.fn(), forkSession: vi.fn(async () => {}), archiveSession: vi.fn(async (_sessionId: SessionId) => {}),
|
||||
}
|
||||
const controls = createWorkspaceShortcutControls()
|
||||
const fiber = ctx.plugin((scoped) => { installWorkspaceShortcuts(scoped, navigation as unknown as UiWorkspace, controls, navigation.archiveSession) })
|
||||
const fiber = ctx.plugin((scoped) => {
|
||||
installWorkspaceShortcuts(scoped, navigation as unknown as UiWorkspace, controls, (id) => { void navigation.archiveSession(id) })
|
||||
})
|
||||
await fiber.await()
|
||||
const select = (id: string) => { list.set({ ...list.getSnapshot(), byId: {
|
||||
[sid('a')]: row('a', id === 'a'), [sid('b')]: row('b', id === 'b'),
|
||||
|
||||
@@ -696,7 +696,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
|
||||
],
|
||||
replaceRisk: 'shadows-shipped-ui',
|
||||
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'conversation.hero.workspace.directoryFlow\', () => ctx.slots.register(\n { name: \'conversation.hero.workspace.directoryFlow\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
|
||||
source: 'packages/client/ui-workspace/src/client/contract/slots.ts:99',
|
||||
source: 'packages/client/ui-workspace/src/client/contract/slots.ts:101',
|
||||
},
|
||||
{
|
||||
key: 'conversation.input.activity',
|
||||
@@ -1760,7 +1760,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
|
||||
],
|
||||
replaceRisk: 'shadows-shipped-ui',
|
||||
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'main\', () => ctx.slots.register(\n { name: \'main\', key: \'<one key the owner dispatches>\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
|
||||
source: 'packages/client/ui-layout/src/client/index.ts:66',
|
||||
source: 'packages/client/ui-layout/src/client/index.ts:73',
|
||||
},
|
||||
{
|
||||
key: 'main.conversation',
|
||||
@@ -2147,7 +2147,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
|
||||
],
|
||||
replaceRisk: 'shadows-shipped-ui',
|
||||
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'rightbar\', () => ctx.slots.register(\n { name: \'rightbar\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
|
||||
source: 'packages/client/ui-layout/src/client/index.ts:80',
|
||||
source: 'packages/client/ui-layout/src/client/index.ts:87',
|
||||
},
|
||||
{
|
||||
key: 'rightbar.session',
|
||||
@@ -2189,7 +2189,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
|
||||
],
|
||||
replaceRisk: 'shadows-shipped-ui',
|
||||
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'rightbar.session\', () => ctx.slots.register(\n { name: \'rightbar.session\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
|
||||
source: 'packages/client/ui-sidebar-right/src/client/contract/slots.ts:42',
|
||||
source: 'packages/client/ui-sidebar-right/src/client/contract/slots.ts:43',
|
||||
},
|
||||
{
|
||||
key: 'root',
|
||||
@@ -2355,6 +2355,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
|
||||
'client-ui-permission-presets PermissionRow id \'permission\'',
|
||||
'client-ui-settings-general DeveloperToolsRow id \'developer-tools\'',
|
||||
'client-ui-settings-general CurrentVersionRow id \'current-version\'',
|
||||
'client-ui-shortcuts ShortcutsRow id \'shortcuts\'',
|
||||
'client-ui-theme AppearanceRow id \'appearance\'',
|
||||
'client-ui-theme FontSizeRow id \'font-size\'',
|
||||
],
|
||||
@@ -2752,7 +2753,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
|
||||
],
|
||||
replaceRisk: 'shadows-shipped-ui',
|
||||
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'shell.leading\', () => ctx.slots.register(\n { name: \'shell.leading\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
|
||||
source: 'packages/client/ui-layout/src/client/index.ts:105',
|
||||
source: 'packages/client/ui-layout/src/client/index.ts:112',
|
||||
},
|
||||
{
|
||||
key: 'shell.overlay',
|
||||
@@ -2796,13 +2797,14 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
|
||||
slotInject: '',
|
||||
declaredBy: 'an entry in \'root\' (client-ui-layout), so it exists while that entry is mounted',
|
||||
occupants: [
|
||||
'client-ui-shortcuts ShortcutReference id \'shortcuts\'',
|
||||
'client-ui-workspace SessionRenameDialog id \'workspace.session-rename\'',
|
||||
'client-ui-workspace SessionArchiveConfirmDialog id \'workspace.session-archive\'',
|
||||
'client-ui-workspace RowActionToast id \'workspace.row-toast\'',
|
||||
],
|
||||
replaceRisk: 'none',
|
||||
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'shell.overlay\', () => ctx.slots.register(\n { name: \'shell.overlay\', id: \'my-entry\', order: 100, label: \'My entry\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
|
||||
source: 'packages/client/ui-layout/src/client/index.ts:91',
|
||||
source: 'packages/client/ui-layout/src/client/index.ts:98',
|
||||
},
|
||||
{
|
||||
key: 'sidebar',
|
||||
@@ -2833,7 +2835,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
|
||||
],
|
||||
replaceRisk: 'shadows-shipped-ui',
|
||||
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'sidebar\', () => ctx.slots.register(\n { name: \'sidebar\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
|
||||
source: 'packages/client/ui-layout/src/client/index.ts:61',
|
||||
source: 'packages/client/ui-layout/src/client/index.ts:68',
|
||||
},
|
||||
{
|
||||
key: 'sidebar.brand.mark',
|
||||
@@ -2864,7 +2866,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
|
||||
],
|
||||
replaceRisk: 'shadows-shipped-ui',
|
||||
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'sidebar.brand.mark\', () => ctx.slots.register(\n { name: \'sidebar.brand.mark\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
|
||||
source: 'packages/client/ui-sidebar/src/client/contract/slots.ts:24',
|
||||
source: 'packages/client/ui-sidebar/src/client/contract/slots.ts:25',
|
||||
},
|
||||
{
|
||||
key: 'sidebar.brand.name',
|
||||
@@ -2895,7 +2897,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
|
||||
],
|
||||
replaceRisk: 'shadows-shipped-ui',
|
||||
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'sidebar.brand.name\', () => ctx.slots.register(\n { name: \'sidebar.brand.name\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
|
||||
source: 'packages/client/ui-sidebar/src/client/contract/slots.ts:29',
|
||||
source: 'packages/client/ui-sidebar/src/client/contract/slots.ts:30',
|
||||
},
|
||||
{
|
||||
key: 'sidebar.chat.conversation',
|
||||
@@ -2982,7 +2984,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
|
||||
],
|
||||
replaceRisk: 'none',
|
||||
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'sidebar.footer.action\', () => ctx.slots.register(\n { name: \'sidebar.footer.action\', id: \'my-entry\', order: 100, label: \'My entry\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
|
||||
source: 'packages/client/ui-sidebar/src/client/contract/slots.ts:52',
|
||||
source: 'packages/client/ui-sidebar/src/client/contract/slots.ts:53',
|
||||
},
|
||||
{
|
||||
key: 'sidebar.panellist',
|
||||
@@ -3032,7 +3034,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
|
||||
],
|
||||
replaceRisk: 'none',
|
||||
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'sidebar.panellist\', () => ctx.slots.register(\n { name: \'sidebar.panellist\', id: \'my-entry\', order: 100, label: \'My entry\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
|
||||
source: 'packages/client/ui-sidebar/src/client/contract/slots.ts:34',
|
||||
source: 'packages/client/ui-sidebar/src/client/contract/slots.ts:35',
|
||||
},
|
||||
{
|
||||
key: 'sidebar.right.pane.tab',
|
||||
@@ -3083,7 +3085,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
|
||||
],
|
||||
replaceRisk: 'none',
|
||||
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'sidebar.right.pane.tab\', () => ctx.slots.register(\n { name: \'sidebar.right.pane.tab\', key: \'<one key the owner dispatches>\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
|
||||
source: 'packages/client/ui-sidebar-right/src/client/contract/slots.ts:58',
|
||||
source: 'packages/client/ui-sidebar-right/src/client/contract/slots.ts:59',
|
||||
},
|
||||
{
|
||||
key: 'sidebar.right.pane.tab.title',
|
||||
@@ -3132,7 +3134,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
|
||||
],
|
||||
replaceRisk: 'none',
|
||||
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'sidebar.right.pane.tab.title\', () => ctx.slots.register(\n { name: \'sidebar.right.pane.tab.title\', key: \'<one key the owner dispatches>\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
|
||||
source: 'packages/client/ui-sidebar-right/src/client/contract/slots.ts:72',
|
||||
source: 'packages/client/ui-sidebar-right/src/client/contract/slots.ts:73',
|
||||
},
|
||||
{
|
||||
key: 'sidebar.right.tab.document',
|
||||
@@ -3437,7 +3439,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
|
||||
occupants: [],
|
||||
replaceRisk: 'none',
|
||||
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'sidebar.right.tab.guide\', () => ctx.slots.register(\n { name: \'sidebar.right.tab.guide\', select: owner => null },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
|
||||
source: 'packages/client/ui-sidebar-right/src/client/contract/slots.ts:83',
|
||||
source: 'packages/client/ui-sidebar-right/src/client/contract/slots.ts:84',
|
||||
},
|
||||
{
|
||||
key: 'sidebar.right.tab.guide.entry',
|
||||
@@ -3483,7 +3485,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
|
||||
],
|
||||
replaceRisk: 'none',
|
||||
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'sidebar.right.tab.guide.entry\', () => ctx.slots.register(\n { name: \'sidebar.right.tab.guide.entry\', key: \'<one key the owner dispatches>\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
|
||||
source: 'packages/client/ui-sidebar-right/src/client/contract/slots.ts:90',
|
||||
source: 'packages/client/ui-sidebar-right/src/client/contract/slots.ts:91',
|
||||
},
|
||||
{
|
||||
key: 'sidebar.right.tab.menu.item',
|
||||
@@ -3541,7 +3543,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
|
||||
occupants: [],
|
||||
replaceRisk: 'none',
|
||||
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'sidebar.right.tab.menu.item\', () => ctx.slots.register(\n { name: \'sidebar.right.tab.menu.item\', id: \'my-entry\', order: 100, label: \'My entry\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
|
||||
source: 'packages/client/ui-sidebar-right/src/client/contract/slots.ts:102',
|
||||
source: 'packages/client/ui-sidebar-right/src/client/contract/slots.ts:103',
|
||||
},
|
||||
{
|
||||
key: 'sidebar.settings',
|
||||
@@ -3572,7 +3574,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
|
||||
],
|
||||
replaceRisk: 'shadows-shipped-ui',
|
||||
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'sidebar.settings\', () => ctx.slots.register(\n { name: \'sidebar.settings\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
|
||||
source: 'packages/client/ui-sidebar/src/client/contract/slots.ts:47',
|
||||
source: 'packages/client/ui-sidebar/src/client/contract/slots.ts:48',
|
||||
},
|
||||
{
|
||||
key: 'sidebar.toggle.badge',
|
||||
@@ -3601,7 +3603,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
|
||||
],
|
||||
replaceRisk: 'shadows-shipped-ui',
|
||||
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'sidebar.toggle.badge\', () => ctx.slots.register(\n { name: \'sidebar.toggle.badge\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
|
||||
source: 'packages/client/ui-sidebar/src/client/contract/slots.ts:18',
|
||||
source: 'packages/client/ui-sidebar/src/client/contract/slots.ts:19',
|
||||
},
|
||||
{
|
||||
key: 'sidebar.workspaces',
|
||||
@@ -3632,7 +3634,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
|
||||
],
|
||||
replaceRisk: 'shadows-shipped-ui',
|
||||
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'sidebar.workspaces\', () => ctx.slots.register(\n { name: \'sidebar.workspaces\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
|
||||
source: 'packages/client/ui-sidebar/src/client/contract/slots.ts:41',
|
||||
source: 'packages/client/ui-sidebar/src/client/contract/slots.ts:42',
|
||||
},
|
||||
{
|
||||
key: 'sidebar.workspaces.directoryFlow',
|
||||
@@ -3664,7 +3666,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
|
||||
],
|
||||
replaceRisk: 'shadows-shipped-ui',
|
||||
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'sidebar.workspaces.directoryFlow\', () => ctx.slots.register(\n { name: \'sidebar.workspaces.directoryFlow\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
|
||||
source: 'packages/client/ui-workspace/src/client/contract/slots.ts:101',
|
||||
source: 'packages/client/ui-workspace/src/client/contract/slots.ts:103',
|
||||
},
|
||||
{
|
||||
key: 'sidebar.workspaces.session.menu.item',
|
||||
@@ -3709,7 +3711,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
|
||||
],
|
||||
keyDomain: '',
|
||||
hookContext: 'MenuOpenState',
|
||||
slotInject: '{ hooks: { menuOpenState: SlotHookFactory<\'sidebar.workspaces.session.menu.item\', UseMenuOpenState> } }',
|
||||
slotInject: '{ hooks: { menuOpenState: SlotHookFactory<\'sidebar.workspaces.session.menu.item\', UseMenuOpenState> shortcuts: HostObservable<readonly ShortcutCatalogEntry[]> } }',
|
||||
declaredBy: 'an entry in \'sidebar.workspaces\' (client-ui-workspace), so it exists while that entry is mounted',
|
||||
occupants: [
|
||||
'client-ui-workspace PinSessionMenuItem id \'pin\'',
|
||||
@@ -3719,7 +3721,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
|
||||
],
|
||||
replaceRisk: 'none',
|
||||
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n const copyLabel = \'Copy Session ID\' // Localize in the contributing package.\n ctx.slots.inject(\'sidebar.workspaces.session.menu.item\', () => ctx.slots.register(\n { name: \'sidebar.workspaces.session.menu.item\', id: \'copy-session-id\', order: 500 },\n ({ sessionId, useMenuOpenState }) => {\n const [, setMenuOpen] = useMenuOpenState()\n return React.createElement(\n \'button\',\n { type: \'button\', role: \'menuitem\', onClick: () => { setMenuOpen(false); void navigator.clipboard.writeText(sessionId) } },\n copyLabel,\n )\n },\n ))\n },\n}',
|
||||
source: 'packages/client/ui-workspace/src/client/contract/slots.ts:133',
|
||||
source: 'packages/client/ui-workspace/src/client/contract/slots.ts:135',
|
||||
},
|
||||
{
|
||||
key: 'sidebar.workspaces.session.row.action',
|
||||
@@ -3772,7 +3774,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
|
||||
],
|
||||
replaceRisk: 'none',
|
||||
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'sidebar.workspaces.session.row.action\', () => ctx.slots.register(\n { name: \'sidebar.workspaces.session.row.action\', id: \'my-entry\', order: 100, label: \'My entry\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
|
||||
source: 'packages/client/ui-workspace/src/client/contract/slots.ts:148',
|
||||
source: 'packages/client/ui-workspace/src/client/contract/slots.ts:153',
|
||||
},
|
||||
{
|
||||
key: 'tool.call.images',
|
||||
|
||||
+2
@@ -63,6 +63,7 @@ Every package below exports a Cordis plugin that a bundle patch can name in a Lo
|
||||
| `@deepseek-ai/dsh-client-locale` | no | Locale plugin: Host-backed preference, extensible language catalog, browser fallback, and typed built-in dictionaries |
|
||||
| `@deepseek-ai/dsh-client-modules` | no | Client module system, dual-face: node half composes the __DSH_BOOT__ entry graph (incremental dsh.client scan, bundle route, index tap, webPlugins service); browser half is the lazy-CJS module table the vendored cordis Loader consumes as its internal seam |
|
||||
| `@deepseek-ai/dsh-client-resources` | no | Unified client resource model: protocol-registered providers turn URL addresses into live values, consumed through the useResource global standard hook |
|
||||
| `@deepseek-ai/dsh-client-shortcuts` | yes | Application keyboard command registry and physical-key routing |
|
||||
| `@deepseek-ai/dsh-client-ui-agent-preset` | no | Agent-preset surfaces: the default for later sessions, this session's seat, and the composition editor |
|
||||
| `@deepseek-ai/dsh-client-ui-approval` | no | Approval composer takeover over the scoped Remote Event waterfall |
|
||||
| `@deepseek-ai/dsh-client-ui-attachment` | no | Dynamic attachment presentation plugin for conversation input, message-image, and trajectory image slots |
|
||||
@@ -97,6 +98,7 @@ Every package below exports a Cordis plugin that a bundle patch can name in a Lo
|
||||
| `@deepseek-ai/dsh-client-ui-settings-shell` | no | Settings page of the shell executor on the dsh web client's Plugins page: the command timeout and the per-stream output cap of the shell namespace |
|
||||
| `@deepseek-ai/dsh-client-ui-settings-subagent` | no | Settings page of Subagent delegation on the dsh web client's Plugins page: recursion depth, parallel capacity, and the models agents may choose for subagents |
|
||||
| `@deepseek-ai/dsh-client-ui-settings-web-search` | no | Settings page of the DeepSeek web-search provider on the dsh web client's Plugins page: its API key, endpoint, and per-request search budget |
|
||||
| `@deepseek-ai/dsh-client-ui-shortcuts` | no | Keyboard shortcut reference, recording, and local preference editing |
|
||||
| `@deepseek-ai/dsh-client-ui-sidebar` | no | Sidebar plugin: session multi-level tree, search, grouping, state dots |
|
||||
| `@deepseek-ai/dsh-client-ui-sidebar-browser` | no | Sandboxed Web browser tabs for the right Sidebar |
|
||||
| `@deepseek-ai/dsh-client-ui-sidebar-documentpreview` | yes | Extensible Sidebar previews for Office documents, spreadsheets, Markdown, code, images, PDF, HTML, and plain text |
|
||||
|
||||
Reference in new issue
Block a user