diff --git a/apps/web/tests/expected/desktop-locale/automatic.expected.md b/apps/web/tests/expected/desktop-locale/automatic.expected.md index 4e10073523..db9488671b 100644 --- a/apps/web/tests/expected/desktop-locale/automatic.expected.md +++ b/apps/web/tests/expected/desktop-locale/automatic.expected.md @@ -25,8 +25,8 @@ - text: 开发者工具 显示用于调试和排查问题的工具与信息 - switch "开发者工具" [checked] - text: 快捷键 - - paragraph: 查看当前可用的快捷键和输入操作 - - button "查看快捷键" + - paragraph: 查看和编辑当前可用的快捷键和输入操作 + - button "编辑快捷键" - text: 繁忙时的发送行为 智能体运行时 Enter 键和发送按钮的行为;Cmd/Ctrl+Enter 使用另一行为 - button "排队发送" - text: 当前版本:{{version}} diff --git a/apps/web/tests/expected/desktop-locale/selected.expected.md b/apps/web/tests/expected/desktop-locale/selected.expected.md index eadbe2289d..d353691f8d 100644 --- a/apps/web/tests/expected/desktop-locale/selected.expected.md +++ b/apps/web/tests/expected/desktop-locale/selected.expected.md @@ -25,8 +25,8 @@ - text: Developer tools Show tools and information for debugging and troubleshooting - switch "Developer tools" [checked] - text: Keyboard shortcuts - - paragraph: Browse available shortcuts and input actions - - button "View shortcuts" + - paragraph: View and edit available shortcuts and input actions + - button "Edit shortcuts" - text: Send behavior while busy What Enter and the Send button do while the agent is running; Cmd/Ctrl+Enter uses the other behavior - button "Queue" - text: "Current version: {{version}}" diff --git a/apps/web/tests/expected/settings-chrome/dialog-en.expected.md b/apps/web/tests/expected/settings-chrome/dialog-en.expected.md index 2ad9903a2e..58e7b684a4 100644 --- a/apps/web/tests/expected/settings-chrome/dialog-en.expected.md +++ b/apps/web/tests/expected/settings-chrome/dialog-en.expected.md @@ -25,8 +25,8 @@ - text: Developer tools Show tools and information for debugging and troubleshooting - switch "Developer tools" - text: Keyboard shortcuts - - paragraph: Browse available shortcuts and input actions - - button "View shortcuts" + - paragraph: View and edit available shortcuts and input actions + - button "Edit shortcuts" - text: Send behavior while busy What Enter and the Send button do while the agent is running; Cmd/Ctrl+Enter uses the other behavior - button "Queue" - text: "Current version: {{version}}" diff --git a/apps/web/tests/expected/settings-chrome/dialog-no-browser.expected.md b/apps/web/tests/expected/settings-chrome/dialog-no-browser.expected.md index 2ad9903a2e..58e7b684a4 100644 --- a/apps/web/tests/expected/settings-chrome/dialog-no-browser.expected.md +++ b/apps/web/tests/expected/settings-chrome/dialog-no-browser.expected.md @@ -25,8 +25,8 @@ - text: Developer tools Show tools and information for debugging and troubleshooting - switch "Developer tools" - text: Keyboard shortcuts - - paragraph: Browse available shortcuts and input actions - - button "View shortcuts" + - paragraph: View and edit available shortcuts and input actions + - button "Edit shortcuts" - text: Send behavior while busy What Enter and the Send button do while the agent is running; Cmd/Ctrl+Enter uses the other behavior - button "Queue" - text: "Current version: {{version}}" diff --git a/apps/web/tests/expected/settings-chrome/dialog.expected.md b/apps/web/tests/expected/settings-chrome/dialog.expected.md index a4a9349ffb..a9cc91d984 100644 --- a/apps/web/tests/expected/settings-chrome/dialog.expected.md +++ b/apps/web/tests/expected/settings-chrome/dialog.expected.md @@ -25,8 +25,8 @@ - text: 开发者工具 显示用于调试和排查问题的工具与信息 - switch "开发者工具" - text: 快捷键 - - paragraph: 查看当前可用的快捷键和输入操作 - - button "查看快捷键" + - paragraph: 查看和编辑当前可用的快捷键和输入操作 + - button "编辑快捷键" - text: 繁忙时的发送行为 智能体运行时 Enter 键和发送按钮的行为;Cmd/Ctrl+Enter 使用另一行为 - button "排队发送" - text: 当前版本:{{version}} diff --git a/apps/web/tests/expected/shortcuts-desktop/chord.expected.md b/apps/web/tests/expected/shortcuts-desktop/chord.expected.md index 3bf739802b..3105f57dec 100644 --- a/apps/web/tests/expected/shortcuts-desktop/chord.expected.md +++ b/apps/web/tests/expected/shortcuts-desktop/chord.expected.md @@ -1,5 +1,3 @@ - listitem: - - text: New Session - - button "Remove shortcut for New Session" - button "Edit shortcut for New Session" - - button "New Session A B": A B + - text: New Session A B diff --git a/apps/web/tests/expected/shortcuts-desktop/macos-copy-override.expected.md b/apps/web/tests/expected/shortcuts-desktop/macos-copy-override.expected.md index 0881936876..9979080ca0 100644 --- a/apps/web/tests/expected/shortcuts-desktop/macos-copy-override.expected.md +++ b/apps/web/tests/expected/shortcuts-desktop/macos-copy-override.expected.md @@ -1,5 +1,3 @@ - listitem: - - text: New Session - - button "Remove shortcut for New Session" - button "Edit shortcut for New Session" - - button "New Session ⌘ C": ⌘ C + - text: New Session ⌘ C diff --git a/apps/web/tests/expected/shortcuts-desktop/windows-copy-override.expected.md b/apps/web/tests/expected/shortcuts-desktop/windows-copy-override.expected.md index fd3ec59c13..84272e6570 100644 --- a/apps/web/tests/expected/shortcuts-desktop/windows-copy-override.expected.md +++ b/apps/web/tests/expected/shortcuts-desktop/windows-copy-override.expected.md @@ -1,5 +1,3 @@ - listitem: - - text: New Session - - button "Remove shortcut for New Session" - button "Edit shortcut for New Session" - - button "New Session Ctrl + C": Ctrl + C + - text: New Session Ctrl + C diff --git a/apps/web/tests/expected/shortcuts-workspace/macos-defaults.expected.md b/apps/web/tests/expected/shortcuts-workspace/macos-defaults.expected.md index 7bddffee98..ceed422820 100644 --- a/apps/web/tests/expected/shortcuts-workspace/macos-defaults.expected.md +++ b/apps/web/tests/expected/shortcuts-workspace/macos-defaults.expected.md @@ -7,126 +7,72 @@ - region "Application": - list: - listitem: - - text: Open keyboard shortcuts - - button "Remove shortcut for Open keyboard shortcuts" - button "Edit shortcut for Open keyboard shortcuts" - - button "Open keyboard shortcuts ⌘ /": ⌘ / + - text: Open keyboard shortcuts ⌘ / - listitem: - - text: New Session - - button "Remove shortcut for New Session" - button "Edit shortcut for New Session" - - button "New Session ⌥ ⌘ N": ⌥ ⌘ N + - text: New Session ⌥ ⌘ N - listitem: - - text: Toggle left sidebar - - button "Remove shortcut for Toggle left sidebar" - button "Edit shortcut for Toggle left sidebar" - - button "Toggle left sidebar ⌥ ⌘ B": ⌥ ⌘ B + - text: Toggle left sidebar ⌥ ⌘ B - listitem: - - text: Search sessions - - button "Remove shortcut for Search sessions" - button "Edit shortcut for Search sessions" - - button "Search sessions ⌥ ⌘ K": ⌥ ⌘ K + - text: Search sessions ⌥ ⌘ K - listitem: - - text: Add workspace - - button "Remove shortcut for Add workspace" - button "Edit shortcut for Add workspace" - - button "Add workspace ⌥ ⌘ O": ⌥ ⌘ O + - text: Add workspace ⌥ ⌘ O - listitem: - - text: Rename session - - button "Remove shortcut for Rename session" - button "Edit shortcut for Rename session" - - button "Rename session ⇧ ⌘ R": ⇧ ⌘ R + - text: Rename session ⇧ ⌘ R - listitem: - - text: Fork session - - button "Remove shortcut for Fork session" - button "Edit shortcut for Fork session" - - button "Fork session ⇧ ⌘ F": ⇧ ⌘ F + - text: Fork session ⇧ ⌘ F - listitem: - - text: Archive session - - button "Remove shortcut for Archive session" - button "Edit shortcut for Archive session" - - button "Archive session ⌥ ⌘ A": ⌥ ⌘ A + - text: Archive session ⌥ ⌘ A - listitem: - - text: Open settings - - button "Remove shortcut for Open settings" - button "Edit shortcut for Open settings" - - button "Open settings ⌘ ,": ⌘ , + - text: Open settings ⌘ , - listitem: - - text: Toggle right sidebar - - button "Remove shortcut for Toggle right sidebar" - button "Edit shortcut for Toggle right sidebar" - - button "Toggle right sidebar ⇧ ⌘ B": ⇧ ⌘ B + - text: Toggle right sidebar ⇧ ⌘ B - listitem: - - text: Workspace files - - button "Remove shortcut for Workspace files" - button "Edit shortcut for Workspace files" - - button "Workspace files ⌥ ⌘ P": ⌥ ⌘ P + - text: Workspace files ⌥ ⌘ P - listitem: - - text: New terminal - - button "Remove shortcut for New terminal" - button "Edit shortcut for New terminal" - - 'button "New terminal ⌃ `"': "⌃ `" + - text: "New terminal ⌃ `" - listitem: - - text: Split - - button "Remove shortcut for Split" - button "Edit shortcut for Split" - - button "Split ⌘ \\": ⌘ \ + - text: Split ⌘ \ - listitem: - - text: Toggle panel fullscreen - - button "Remove shortcut for Toggle panel fullscreen" - button "Edit shortcut for Toggle panel fullscreen" - - button "Toggle panel fullscreen ⌥ ⌘ Enter": ⌥ ⌘ Enter + - text: Toggle panel fullscreen ⌥ ⌘ Enter - listitem: - - text: Refresh current page - button "Edit shortcut for Refresh current page" - - button "Refresh current page No shortcut": No shortcut + - text: Refresh current page No shortcut - listitem: - - text: Close current page or window - - button "Remove shortcut for Close current page or window" - button "Edit shortcut for Close current page or window" - - button "Close current page or window ⌥ ⌘ W": ⌥ ⌘ W - - listitem: - - text: Stop generating - - button "Stop generating Esc Esc" [disabled]: Esc Esc + - text: Close current page or window ⌥ ⌘ W - region "Message input": - heading "Message input" [level=3] - list: - - listitem: - - text: Use the complementary Queue / Steer action - - button "Use the complementary Queue / Steer action ⌘ Enter" [disabled]: ⌘ Enter - - listitem: - - text: Open reference menu - - button "Open reference menu @" [disabled]: "@" - - listitem: - - text: New line - - button "New line ⇧ Enter" [disabled]: ⇧ Enter - - listitem: - - text: Send message - - button "Send message Enter" [disabled]: Enter - - listitem: - - text: Open command menu - - button "Open command menu /" [disabled]: / + - listitem: Use the complementary Queue / Steer action ⌘ Enter + - listitem: Open reference menu @ + - listitem: New line ⇧ Enter + - listitem: Send message Enter + - listitem: Open command menu / + - listitem: Stop generating Esc Esc - region "Menus and dialogs": - heading "Menus and dialogs" [level=3] - list: - - listitem: - - text: Close menu or top dialog - - button "Close menu or top dialog Esc" [disabled]: Esc - - listitem: - - text: Move menu selection - - button "Move menu selection ↑ ↓" [disabled]: ↑ ↓ - - listitem: - - text: Select menu item - - button "Select menu item Enter" [disabled]: Enter + - listitem: Close menu or top dialog Esc + - listitem: Move menu selection ↑ ↓ + - listitem: Select menu item Enter - region "Approval area": - heading "Approval area" [level=3] - list: - - listitem: - - text: Allow once - - button "Allow once Enter" [disabled]: Enter - - listitem: - - text: Reject - - button "Reject Esc" [disabled]: Esc + - listitem: Allow once Enter + - listitem: Reject Esc - contentinfo: - button "Restore all defaults" [disabled] - - text: 0 customized diff --git a/apps/web/tests/expected/shortcuts/en-US-empty.expected.md b/apps/web/tests/expected/shortcuts/en-US-empty.expected.md index 0661c89929..94072331f3 100644 --- a/apps/web/tests/expected/shortcuts/en-US-empty.expected.md +++ b/apps/web/tests/expected/shortcuts/en-US-empty.expected.md @@ -4,6 +4,7 @@ - button "Close keyboard shortcuts" - search "Search shortcuts": - searchbox "Search shortcuts": abc + - button "Clear search" - status: No matching shortcuts - contentinfo: - button "Restore all defaults" diff --git a/apps/web/tests/expected/shortcuts/en-US.expected.md b/apps/web/tests/expected/shortcuts/en-US.expected.md index 4460a24ce0..d3740de7a2 100644 --- a/apps/web/tests/expected/shortcuts/en-US.expected.md +++ b/apps/web/tests/expected/shortcuts/en-US.expected.md @@ -7,127 +7,72 @@ - region "Application": - list: - listitem: - - text: Open keyboard shortcuts - - button "Remove shortcut for Open keyboard shortcuts" - button "Edit shortcut for Open keyboard shortcuts" - - button "Open keyboard shortcuts Ctrl + /": Ctrl + / + - text: Open keyboard shortcuts Ctrl + / - listitem: - - text: New Session - - button "Remove shortcut for New Session" - button "Edit shortcut for New Session" - - button "New Session Ctrl + Alt + N": Ctrl + Alt + N + - text: New Session Ctrl + Alt + N - listitem: - - text: Toggle left sidebar - - button "Remove shortcut for Toggle left sidebar" - button "Edit shortcut for Toggle left sidebar" - - button "Toggle left sidebar Ctrl + Alt + B": Ctrl + Alt + B + - text: Toggle left sidebar Ctrl + Alt + B - listitem: - - text: Search sessions - - button "Remove shortcut for Search sessions" - button "Edit shortcut for Search sessions" - - button "Search sessions Ctrl + Alt + K": Ctrl + Alt + K + - text: Search sessions Ctrl + Alt + K - listitem: - - text: Add workspace - - button "Remove shortcut for Add workspace" - button "Edit shortcut for Add workspace" - - button "Add workspace Ctrl + Alt + O": Ctrl + Alt + O + - text: Add workspace Ctrl + Alt + O - listitem: - - text: Rename session - - button "Remove shortcut for Rename session" - button "Edit shortcut for Rename session" - - button "Rename session Ctrl + Shift + R": Ctrl + Shift + R + - text: Rename session Ctrl + Shift + R - listitem: - - text: Fork session - - button "Remove shortcut for Fork session" - button "Edit shortcut for Fork session" - - button "Fork session Ctrl + Shift + F": Ctrl + Shift + F + - text: Fork session Ctrl + Shift + F - listitem: - - text: Archive session - - button "Remove shortcut for Archive session" - button "Edit shortcut for Archive session" - - button "Archive session Ctrl + Alt + A": Ctrl + Alt + A + - text: Archive session Ctrl + Alt + A - listitem: - - text: Open settings - - button "Remove shortcut for Open settings" - button "Edit shortcut for Open settings" - - button "Open settings Ctrl + ,": Ctrl + , + - text: Open settings Ctrl + , - listitem: - - text: Toggle right sidebar - - button "Remove shortcut for Toggle right sidebar" - button "Edit shortcut for Toggle right sidebar" - - button "Toggle right sidebar Ctrl + Shift + B": Ctrl + Shift + B + - text: Toggle right sidebar Ctrl + Shift + B - listitem: - - text: Workspace files - - button "Remove shortcut for Workspace files" - button "Edit shortcut for Workspace files" - - button "Workspace files Ctrl + Alt + P": Ctrl + Alt + P + - text: Workspace files Ctrl + Alt + P - listitem: - - text: New terminal - - button "Remove shortcut for New terminal" - button "Edit shortcut for New terminal" - - 'button "New terminal Ctrl + `"': "Ctrl + `" + - text: "New terminal Ctrl + `" - listitem: - - text: Split - - button "Remove shortcut for Split" - button "Edit shortcut for Split" - - button "Split Ctrl + \\": Ctrl + \ + - text: Split Ctrl + \ - listitem: - - text: Toggle panel fullscreen - - button "Remove shortcut for Toggle panel fullscreen" - button "Edit shortcut for Toggle panel fullscreen" - - button "Toggle panel fullscreen Ctrl + Alt + Enter": Ctrl + Alt + Enter + - text: Toggle panel fullscreen Ctrl + Alt + Enter - listitem: - - text: Refresh current page - - button "Remove shortcut for Refresh current page" - button "Edit shortcut for Refresh current page" - - button "Refresh current page Ctrl + Alt + R": Ctrl + Alt + R + - text: Refresh current page Ctrl + Alt + R - listitem: - - text: Close current page or window - - button "Remove shortcut for Close current page or window" - button "Edit shortcut for Close current page or window" - - button "Close current page or window Ctrl + Alt + W": Ctrl + Alt + W - - listitem: - - text: Stop generating - - button "Stop generating Esc Esc" [disabled]: Esc Esc + - text: Close current page or window Ctrl + Alt + W - region "Message input": - heading "Message input" [level=3] - list: - - listitem: - - text: Use the complementary Queue / Steer action - - button "Use the complementary Queue / Steer action Ctrl + Enter" [disabled]: Ctrl + Enter - - listitem: - - text: Open reference menu - - button "Open reference menu @" [disabled]: "@" - - listitem: - - text: New line - - button "New line Shift + Enter" [disabled]: Shift + Enter - - listitem: - - text: Send message - - button "Send message Enter" [disabled]: Enter - - listitem: - - text: Open command menu - - button "Open command menu /" [disabled]: / + - listitem: Use the complementary Queue / Steer action Ctrl + Enter + - listitem: Open reference menu @ + - listitem: New line Shift + Enter + - listitem: Send message Enter + - listitem: Open command menu / + - listitem: Stop generating Esc Esc - region "Menus and dialogs": - heading "Menus and dialogs" [level=3] - list: - - listitem: - - text: Close menu or top dialog - - button "Close menu or top dialog Esc" [disabled]: Esc - - listitem: - - text: Move menu selection - - button "Move menu selection ↑ ↓" [disabled]: ↑ ↓ - - listitem: - - text: Select menu item - - button "Select menu item Enter" [disabled]: Enter + - listitem: Close menu or top dialog Esc + - listitem: Move menu selection ↑ ↓ + - listitem: Select menu item Enter - region "Approval area": - heading "Approval area" [level=3] - list: - - listitem: - - text: Allow once - - button "Allow once Enter" [disabled]: Enter - - listitem: - - text: Reject - - button "Reject Esc" [disabled]: Esc + - listitem: Allow once Enter + - listitem: Reject Esc - contentinfo: - button "Restore all defaults" [disabled] - - text: 0 customized diff --git a/apps/web/tests/expected/shortcuts/zh-CN-empty.expected.md b/apps/web/tests/expected/shortcuts/zh-CN-empty.expected.md index 8bc73cf849..f43195fe3f 100644 --- a/apps/web/tests/expected/shortcuts/zh-CN-empty.expected.md +++ b/apps/web/tests/expected/shortcuts/zh-CN-empty.expected.md @@ -4,6 +4,7 @@ - button "关闭快捷键" - search "搜索快捷键": - searchbox "搜索快捷键": abc + - button "清空搜索" - status: 没有匹配的快捷键 - contentinfo: - button "恢复全部默认" diff --git a/apps/web/tests/expected/shortcuts/zh-CN.expected.md b/apps/web/tests/expected/shortcuts/zh-CN.expected.md index 8fa16c3e99..ce19e0b876 100644 --- a/apps/web/tests/expected/shortcuts/zh-CN.expected.md +++ b/apps/web/tests/expected/shortcuts/zh-CN.expected.md @@ -7,126 +7,72 @@ - region "应用操作": - list: - listitem: - - text: 快捷键速查 - - button "移除快捷键速查快捷键" - button "修改快捷键速查快捷键" - - button "快捷键速查 ⌘ /": ⌘ / + - text: 快捷键速查 ⌘ / - listitem: - - text: 新会话 - - button "移除新会话快捷键" - button "修改新会话快捷键" - - button "新会话 ⌥ ⌘ N": ⌥ ⌘ N + - text: 新会话 ⌥ ⌘ N - listitem: - - text: 展开/收起左侧栏 - - button "移除展开/收起左侧栏快捷键" - button "修改展开/收起左侧栏快捷键" - - button "展开/收起左侧栏 ⌥ ⌘ B": ⌥ ⌘ B + - text: 展开/收起左侧栏 ⌥ ⌘ B - listitem: - - text: 搜索会话 - - button "移除搜索会话快捷键" - button "修改搜索会话快捷键" - - button "搜索会话 ⌥ ⌘ K": ⌥ ⌘ K + - text: 搜索会话 ⌥ ⌘ K - listitem: - - text: 添加工作区 - - button "移除添加工作区快捷键" - button "修改添加工作区快捷键" - - button "添加工作区 ⌥ ⌘ O": ⌥ ⌘ O + - text: 添加工作区 ⌥ ⌘ O - listitem: - - text: 重命名会话 - - button "移除重命名会话快捷键" - button "修改重命名会话快捷键" - - button "重命名会话 ⇧ ⌘ R": ⇧ ⌘ R + - text: 重命名会话 ⇧ ⌘ R - listitem: - - text: 分叉会话 - - button "移除分叉会话快捷键" - button "修改分叉会话快捷键" - - button "分叉会话 ⇧ ⌘ F": ⇧ ⌘ F + - text: 分叉会话 ⇧ ⌘ F - listitem: - - text: 归档会话 - - button "移除归档会话快捷键" - button "修改归档会话快捷键" - - button "归档会话 ⌥ ⌘ A": ⌥ ⌘ A + - text: 归档会话 ⌥ ⌘ A - listitem: - - text: 打开设置 - - button "移除打开设置快捷键" - button "修改打开设置快捷键" - - button "打开设置 ⌘ ,": ⌘ , + - text: 打开设置 ⌘ , - listitem: - - text: 展开/收起右侧栏 - - button "移除展开/收起右侧栏快捷键" - button "修改展开/收起右侧栏快捷键" - - button "展开/收起右侧栏 ⇧ ⌘ B": ⇧ ⌘ B + - text: 展开/收起右侧栏 ⇧ ⌘ B - listitem: - - text: 工作区文件 - - button "移除工作区文件快捷键" - button "修改工作区文件快捷键" - - button "工作区文件 ⌥ ⌘ P": ⌥ ⌘ P + - text: 工作区文件 ⌥ ⌘ P - listitem: - - text: 新建终端 - - button "移除新建终端快捷键" - button "修改新建终端快捷键" - - 'button "新建终端 ⌃ `"': "⌃ `" + - text: "新建终端 ⌃ `" - listitem: - - text: 分栏 - - button "移除分栏快捷键" - button "修改分栏快捷键" - - button "分栏 ⌘ \\": ⌘ \ + - text: 分栏 ⌘ \ - listitem: - - text: 面板全屏/退出全屏 - - button "移除面板全屏/退出全屏快捷键" - button "修改面板全屏/退出全屏快捷键" - - button "面板全屏/退出全屏 ⌥ ⌘ Enter": ⌥ ⌘ Enter + - text: 面板全屏/退出全屏 ⌥ ⌘ Enter - listitem: - - text: 刷新当前页面 - button "修改刷新当前页面快捷键" - - button "刷新当前页面 暂无快捷键": 暂无快捷键 + - text: 刷新当前页面 暂无快捷键 - listitem: - - text: 关闭当前页面/窗口 - - button "移除关闭当前页面/窗口快捷键" - button "修改关闭当前页面/窗口快捷键" - - button "关闭当前页面/窗口 ⌥ ⌘ W": ⌥ ⌘ W - - listitem: - - text: 停止生成 - - button "停止生成 Esc Esc" [disabled]: Esc Esc + - text: 关闭当前页面/窗口 ⌥ ⌘ W - region "消息输入": - heading "消息输入" [level=3] - list: - - listitem: - - text: 使用互补的 Queue/Steer 方式 - - button "使用互补的 Queue/Steer 方式 ⌘ Enter" [disabled]: ⌘ Enter - - listitem: - - text: 打开引用菜单 - - button "打开引用菜单 @" [disabled]: "@" - - listitem: - - text: 换行 - - button "换行 ⇧ Enter" [disabled]: ⇧ Enter - - listitem: - - text: 发送消息 - - button "发送消息 Enter" [disabled]: Enter - - listitem: - - text: 打开命令菜单 - - button "打开命令菜单 /" [disabled]: / + - listitem: 使用互补的 Queue/Steer 方式 ⌘ Enter + - listitem: 打开引用菜单 @ + - listitem: 换行 ⇧ Enter + - listitem: 发送消息 Enter + - listitem: 打开命令菜单 / + - listitem: 停止生成 Esc Esc - region "菜单与弹层": - heading "菜单与弹层" [level=3] - list: - - listitem: - - text: 关闭菜单或顶层弹窗 - - button "关闭菜单或顶层弹窗 Esc" [disabled]: Esc - - listitem: - - text: 移动菜单选择 - - button "移动菜单选择 ↑ ↓" [disabled]: ↑ ↓ - - listitem: - - text: 选择菜单项 - - button "选择菜单项 Enter" [disabled]: Enter + - listitem: 关闭菜单或顶层弹窗 Esc + - listitem: 移动菜单选择 ↑ ↓ + - listitem: 选择菜单项 Enter - region "审批区域": - heading "审批区域" [level=3] - list: - - listitem: - - text: 允许一次 - - button "允许一次 Enter" [disabled]: Enter - - listitem: - - text: 拒绝 - - button "拒绝 Esc" [disabled]: Esc + - listitem: 允许一次 Enter + - listitem: 拒绝 Esc - contentinfo: - button "恢复全部默认" [disabled] - - text: 0 项已自定义 diff --git a/apps/web/tests/shortcuts-panels.e2e.ts b/apps/web/tests/shortcuts-panels.e2e.ts index 9f3bd76b10..9968341f1b 100644 --- a/apps/web/tests/shortcuts-panels.e2e.ts +++ b/apps/web/tests/shortcuts-panels.e2e.ts @@ -23,9 +23,9 @@ async function bind(page: Page, primary: string, label: string, previous?: strin expect(await row.getByText('Unavailable', { exact: true }).count()).toBe(0) } if (previous !== undefined) { - const remove = dialog.getByRole('button', { name: `Remove shortcut for ${previous}`, exact: true }) - await remove.click() - await remove.waitFor({ state: 'hidden' }) + await dialog.getByRole('button', { name: `Edit shortcut for ${previous}`, exact: true }).click() + await dialog.getByRole('button', { name: 'Remove', exact: true }).click() + await dialog.getByRole('group').waitFor({ state: 'hidden' }) } await dialog.getByRole('button', { name: `Edit shortcut for ${label}`, exact: true }).click() const recorder = dialog.getByRole('group', { name: label, exact: true }) @@ -125,6 +125,19 @@ describe.skipIf(process.platform === 'win32')('Web sidebar shortcuts', () => { await terminalGuide.focus() expect(await page.getByRole('tooltip').count()).toBe(0) + const terminalEntry = panel.locator('[data-sidebar-right-guide-entry="terminal"]') + const shellMenu = terminalEntry.getByRole('button', { name: 'Choose shell', exact: true }) + const titleBounds = await terminalEntry.getByText('New terminal', { exact: true }).boundingBox() + const arrowBounds = await shellMenu.boundingBox() + expect(arrowBounds!.x).toBeGreaterThanOrEqual(titleBounds!.x + titleBounds!.width) + expect(arrowBounds!.x - titleBounds!.x - titleBounds!.width).toBeLessThan(8) + await shellMenu.click() + await page.getByRole('menu').waitFor() + expect(await panel.locator('[data-sidebar-terminal]').count()).toBe(0) + await page.keyboard.press('Escape') + await page.getByRole('menu').waitFor({ state: 'detached' }) + expect(await shellMenu.evaluate(element => element === document.activeElement)).toBe(true) + await bind(page, primary, 'Browser', 'Toggle right sidebar') const browserGuide = panel.locator('[data-sidebar-right-guide-entry="browser"]') await browserGuide.hover() diff --git a/apps/web/tests/shortcuts-workspace.e2e.ts b/apps/web/tests/shortcuts-workspace.e2e.ts index f269d80e55..e84cdc280d 100644 --- a/apps/web/tests/shortcuts-workspace.e2e.ts +++ b/apps/web/tests/shortcuts-workspace.e2e.ts @@ -61,8 +61,9 @@ describe.skipIf(mode === 'record')('web e2e: workspace shortcuts', () => { expect(await row.getByText('Unavailable', { exact: true }).count()).toBe(0) } if (previous !== undefined) { - await reference.getByRole('button', { name: `Remove shortcut for ${previous}`, exact: true }).click() - await expect.poll(() => reference.getByRole('button', { name: `Remove shortcut for ${previous}`, exact: true }).count()).toBe(0) + await reference.getByRole('button', { name: `Edit shortcut for ${previous}`, exact: true }).click() + await reference.getByRole('button', { name: 'Remove', exact: true }).click() + await reference.getByRole('group').waitFor({ state: 'hidden' }) } await reference.getByRole('button', { name: `Edit shortcut for ${label}`, exact: true }).click() await page.keyboard.press('Meta+Shift+Comma') diff --git a/apps/web/tests/shortcuts.e2e.ts b/apps/web/tests/shortcuts.e2e.ts index b25d56f07a..3bbc15e0eb 100644 --- a/apps/web/tests/shortcuts.e2e.ts +++ b/apps/web/tests/shortcuts.e2e.ts @@ -19,8 +19,8 @@ describe('web e2e: shortcut reference', () => { afterAll(async () => { await browser?.close(); await scaffold?.close() }) it.each([ - { locale: 'zh-CN', platform: 'MacIntel', title: '快捷键', settings: '设置', view: '查看快捷键', search: '搜索快捷键', key: 'Meta' }, - { locale: 'en-US', platform: 'Win32', title: 'Keyboard shortcuts', settings: 'Settings', view: 'View shortcuts', search: 'Search shortcuts', key: 'Control' }, + { locale: 'zh-CN', platform: 'MacIntel', title: '快捷键', settings: '设置', view: '编辑快捷键', search: '搜索快捷键', key: 'Meta' }, + { locale: 'en-US', platform: 'Win32', title: 'Keyboard shortcuts', settings: 'Settings', view: 'Edit shortcuts', search: 'Search shortcuts', key: 'Control' }, ])('opens, searches, and restores nested focus in $locale', async ({ locale, platform, title, settings, view, search, key }) => { const referenceKey = platform === 'Win32' ? 'Control+Slash' : 'Meta+Slash' const context = await browser.newContext({ locale, colorScheme: 'light', viewport: { width: 1440, height: 1000 } }) @@ -46,14 +46,13 @@ describe('web e2e: shortcut reference', () => { const resetAll = dialog.getByRole('button', { name: locale === 'zh-CN' ? '恢复全部默认' : 'Restore all defaults', exact: true }) expect(await resetAll.isDisabled()).toBe(true) expect(await dialog.boundingBox()).toMatchObject({ width: 480, height: 600 }) - expect(await dialog.getByRole('search').boundingBox()).toMatchObject({ width: 432, height: 36 }) + expect(await dialog.getByRole('search').boundingBox()).toMatchObject({ width: 440, height: 36 }) const rows = dialog.getByRole('listitem') - const stopRow = dialog.getByRole('region', { name: locale === 'zh-CN' ? '应用操作' : 'Application', exact: true }) + const stopRow = dialog.getByRole('region', { name: locale === 'zh-CN' ? '消息输入' : 'Message input', exact: true }) .getByRole('listitem').last() await stopRow.getByText(locale === 'zh-CN' ? '停止生成' : 'Stop generating', { exact: true }).waitFor() const stopButtons = stopRow.getByRole('button') - expect(await stopButtons.count()).toBe(1) - expect(await stopButtons.isDisabled()).toBe(true) + expect(await stopButtons.count()).toBe(0) expect(new Set(await rows.evaluateAll(items => items.map(row => row.getBoundingClientRect().height)))) .toEqual(new Set([42])) expect(await rows.evaluateAll(items => items.every(row => row.getBoundingClientRect().height >= 42 @@ -72,29 +71,22 @@ describe('web e2e: shortcut reference', () => { const dialogShadow = await dialog.evaluate(element => getComputedStyle(element).boxShadow) await compareOrRefreshGolden(join(expected, `${locale}.expected.md`), await captureStableAria(page, '[data-shortcut-modal="shortcuts"]', scaffold.workspaceCwd), mode) - const fixedButtons = dialog.getByRole('button', { disabled: true }) - const fixedButton = fixedButtons.first() - expect(await fixedButton.evaluate(element => getComputedStyle(element).opacity)).toBe('0.45') - const fixedBackground = await fixedButton.locator('span').evaluate(element => getComputedStyle(element).backgroundColor) - await fixedButton.hover() - expect(await fixedButton.locator('span').evaluate(element => getComputedStyle(element).backgroundColor)).toBe(fixedBackground) + const fixedBackground = await stopRow.evaluate(element => getComputedStyle(element).backgroundColor) + await stopRow.hover() + expect(await stopRow.evaluate(element => getComputedStyle(element).backgroundColor)).toBe(fixedBackground) const editLabel = locale === 'zh-CN' ? '修改展开/收起左侧栏快捷键' : 'Edit shortcut for Toggle left sidebar' - const removeLabel = locale === 'zh-CN' ? '移除展开/收起左侧栏快捷键' : 'Remove shortcut for Toggle left sidebar' const row = dialog.getByRole('listitem').filter({ has: page.getByRole('button', { name: editLabel, exact: true }) }) const editButton = page.getByRole('button', { name: editLabel, exact: true }) - expect(await editButton.evaluate(element => getComputedStyle(element.parentElement!).opacity)).toBe('0') + expect(await row.locator('[class*="rowActions"]').evaluate(element => getComputedStyle(element).opacity)).toBe('0') const boundRow = dialog.getByRole('listitem').filter({ has: page.getByText( locale === 'zh-CN' ? '快捷键速查' : 'Open keyboard shortcuts', { exact: true }, ) }) - const badge = boundRow.getByRole('button').last() - const badgeColor = () => badge.locator('span').evaluate(element => getComputedStyle(element).backgroundColor) - const restingColor = await badgeColor() - await boundRow.locator(':scope > span').first().hover() - expect(await badgeColor()).toBe(restingColor) - await badge.hover() - expect(await badgeColor()).toBe('rgba(38, 49, 72, 0.14)') - await boundRow.getByRole('button').first().hover() - expect(await badgeColor()).toBe(restingColor) + const badge = boundRow.getByRole('button', { name: locale === 'zh-CN' ? '修改快捷键速查快捷键' : 'Edit shortcut for Open keyboard shortcuts', exact: true }) + const restingColor = await boundRow.evaluate(element => getComputedStyle(element).backgroundColor) + await badge.hover({ position: { x: 12, y: 12 } }) + expect(await boundRow.evaluate(element => getComputedStyle(element).backgroundColor)).not.toBe(restingColor) + expect(await boundRow.evaluate(element => getComputedStyle(element).borderRadius)).toBe('12px') + expect(await badge.boundingBox()).toEqual(await boundRow.boundingBox()) await badge.click() await boundRow.getByRole('group').waitFor() expect(await searchSurfaceStyle()).toEqual(focusedSearchSurface) @@ -104,7 +96,7 @@ describe('web e2e: shortcut reference', () => { await page.keyboard.press('Escape') await boundRow.getByRole('group').waitFor({ state: 'hidden' }) await dialog.getByRole('heading', { name: title, exact: true }).hover() - const boundActionsOpacity = () => boundRow.getByRole('button').first().evaluate(element => getComputedStyle(element.parentElement!).opacity) + const boundActionsOpacity = () => boundRow.locator('[class*="rowActions"]').evaluate(element => getComputedStyle(element).opacity) expect(await boundActionsOpacity()).toBe('0') expect(await dialog.evaluate(element => element === document.activeElement)).toBe(true) expect(await dialog.evaluate(element => getComputedStyle(element).outlineStyle)).toBe('none') @@ -118,14 +110,17 @@ describe('web e2e: shortcut reference', () => { await dialog.getByRole('heading', { name: title, exact: true }).hover() expect(await boundActionsOpacity()).toBe('0') await row.hover() - expect(await editButton.evaluate(element => getComputedStyle(element.parentElement!).opacity)).toBe('1') - expect(await row.getByRole('button', { name: removeLabel, exact: true }).count()).toBe(1) + expect(await row.locator('[class*="rowActions"]').evaluate(element => getComputedStyle(element).opacity)).toBe('1') + expect(await row.getByRole('button').count()).toBe(1) await editButton.click() const inline = dialog.getByRole('group', { name: locale === 'zh-CN' ? '展开/收起左侧栏' : 'Toggle left sidebar', exact: true }) const recorder = inline.getByRole('button', { name: locale === 'zh-CN' ? '按下快捷键' : 'Press a shortcut', exact: true }) expect(await inline.getByRole('button', { name: locale === 'zh-CN' ? '移除' : 'Remove', exact: true }).count()).toBe(1) expect(await page.getByRole('dialog').count()).toBe(2) - expect(await recorder.evaluate(element => getComputedStyle(element).color)).toBe('rgb(129, 133, 140)') + expect(await recorder.evaluate(element => ({ + color: getComputedStyle(element).color, border: getComputedStyle(element).borderColor, + }))) + .toEqual({ color: 'rgb(97, 102, 107)', border: 'rgba(0, 0, 0, 0.16)' }) expect(await recorder.evaluate(element => ({ focused: element === document.activeElement, outline: getComputedStyle(element).outlineStyle, shadow: getComputedStyle(element).boxShadow, @@ -155,7 +150,7 @@ describe('web e2e: shortcut reference', () => { expect(await dialog.evaluate(element => getComputedStyle(element).boxShadow)).toBe(dialogShadow) expect(await editButton.evaluate(element => element.matches(':focus-visible'))).toBe(false) await dialog.getByRole('heading', { name: title, exact: true }).hover() - expect(await editButton.evaluate(element => getComputedStyle(element.parentElement!).opacity)).toBe('0') + expect(await row.locator('[class*="rowActions"]').evaluate(element => getComputedStyle(element).opacity)).toBe('0') await page.keyboard.press('Shift+Tab') expect(await resetAll.evaluate(element => element === document.activeElement)).toBe(true) expect(await resetAll.evaluate(element => getComputedStyle(element).outlineStyle)).not.toBe('none') @@ -168,12 +163,14 @@ describe('web e2e: shortcut reference', () => { }))).toEqual({ background: 'rgb(67, 69, 74)', color: 'rgb(255, 255, 255)' }) await page.emulateMedia({ colorScheme: 'light' }) await expect.poll(() => successToast.evaluate(element => getComputedStyle(element).backgroundColor)).toBe('rgb(53, 54, 56)') - await page.getByRole('button', { name: removeLabel, exact: true }).click() + await editButton.click() + await inline.getByRole('button', { name: locale === 'zh-CN' ? '移除' : 'Remove', exact: true }).click() + await inline.waitFor({ state: 'hidden' }) const unbound = row.getByText(locale === 'zh-CN' ? '暂无快捷键' : 'No shortcut', { exact: true }) await unbound.waitFor() expect(await unbound.evaluate(element => getComputedStyle(element).color)).toBe('rgb(129, 133, 140)') - expect(await row.getByRole('button', { name: removeLabel, exact: true }).count()).toBe(0) - await unbound.click() + expect(await row.getByRole('button').count()).toBe(1) + await editButton.click({ position: { x: 12, y: 12 } }) await inline.waitFor() expect(await inline.getByRole('button', { name: locale === 'zh-CN' ? '移除' : 'Remove', exact: true }).count()).toBe(0) await page.keyboard.press('Escape') @@ -182,7 +179,7 @@ describe('web e2e: shortcut reference', () => { expect(await dialog.evaluate(element => getComputedStyle(element).outlineStyle)).toBe('none') expect(await dialog.evaluate(element => getComputedStyle(element).boxShadow)).toBe(dialogShadow) await dialog.getByRole('heading', { name: title, exact: true }).hover() - expect(await editButton.evaluate(element => getComputedStyle(element.parentElement!).opacity)).toBe('0') + expect(await row.getByRole('button').count()).toBe(1) expect(await page.getByRole('dialog').count()).toBe(2) for (const query of ['abc', 'sendEnter']) { await page.getByRole('searchbox').fill(query) @@ -245,11 +242,13 @@ describe('web e2e: shortcut reference', () => { await page.setViewportSize({ width: 1440, height: 1000 }) await openSettings(page, locale === 'zh-CN' ? 'zh' : 'en') const viewButton = page.getByRole('button', { name: view, exact: true }) - const viewHint = `${view} ${key === 'Meta' ? '⌘ /' : 'Ctrl + /'}` + const viewHint = `${locale === 'zh-CN' ? '全局唤起查看' : 'Open from anywhere'} ${key === 'Meta' ? '⌘ /' : 'Ctrl + /'}` await viewButton.hover() - expect(await page.getByRole('tooltip').textContent()).toBe(viewHint) + expect(await page.getByRole('tooltip').getAttribute('aria-label')).toBe(viewHint) await viewButton.click() - await boundRow.getByRole('button').first().click() + await badge.click() + await boundRow.getByRole('button', { name: locale === 'zh-CN' ? '移除' : 'Remove', exact: true }).click() + await boundRow.getByRole('group').waitFor({ state: 'hidden' }) await boundRow.getByText(locale === 'zh-CN' ? '暂无快捷键' : 'No shortcut', { exact: true }).waitFor() await page.keyboard.press('Escape') await dialog.waitFor({ state: 'hidden' }) @@ -266,7 +265,7 @@ describe('web e2e: shortcut reference', () => { await page.keyboard.press('Escape') await dialog.waitFor({ state: 'hidden' }) await viewButton.hover() - expect(await page.getByRole('tooltip').textContent()).toBe(viewHint) + expect(await page.getByRole('tooltip').getAttribute('aria-label')).toBe(viewHint) await viewButton.click() await page.getByRole('searchbox').fill('abc') expect(await dialog.getByRole('listitem').count()).toBe(0) @@ -286,7 +285,7 @@ describe('web e2e: shortcut reference', () => { await confirm.getByRole('button', { name: locale === 'zh-CN' ? '恢复默认' : 'Restore default', exact: true }).click() await confirm.waitFor({ state: 'hidden' }) expect(await resetAll.isDisabled()).toBe(true) - expect(await dialog.locator('footer').textContent()).toContain(locale === 'zh-CN' ? '0 项已自定义' : '0 customized') + expect(await dialog.locator('footer').textContent()).toBe(locale === 'zh-CN' ? '恢复全部默认' : 'Restore all defaults') expect(await page.getByRole('searchbox').evaluate(element => element === document.activeElement)).toBe(true) expect(await searchInput.evaluate(element => getComputedStyle(element).outlineStyle)).toBe('none') expect(await searchSurfaceStyle()).toEqual(focusedSearchSurface) @@ -322,6 +321,49 @@ describe('web e2e: shortcut reference', () => { } finally { await context.close() } }) + it.each([ + { platform: 'Win32', primary: 'Control', keys: 'Ctrl + Alt + N' }, + { platform: 'MacIntel', primary: 'Meta', keys: '⌥ ⇧ ⌘ F12' }, + ])('keeps New Session centered and replaces a long inline binding with a key-only tooltip on $platform', async ({ platform, primary, keys }) => { + const context = await browser.newContext({ locale: 'en-US', viewport: { width: 1440, height: 1000 } }) + try { + await context.addInitScript((value) => { Object.defineProperty(navigator, 'platform', { value }) }, platform) + const page = await context.newPage() + await page.goto(scaffold.authenticatedUrl) + const newSession = page.getByRole('button', { name: 'New session', exact: true }).filter({ hasText: 'New Session' }) + await newSession.waitFor() + await page.keyboard.press(`${primary}+/`) + await page.getByRole('button', { name: 'Edit shortcut for New Session', exact: true }).click() + await page.keyboard.press(platform === 'Win32' ? 'Control+Alt+N' : 'Meta+Alt+Shift+F12') + await page.getByRole('group', { name: 'New Session', exact: true }).waitFor({ state: 'hidden' }) + await page.keyboard.press('Escape') + for (const [width, fits] of [[420, true], [264, false]] as const) { + const handle = (await page.locator('[data-side="sidebar"]').boundingBox())! + await page.mouse.move(handle.x + handle.width / 2, 240) + await page.mouse.down() + await page.mouse.move(width, 240, { steps: 10 }) + await page.mouse.up() + await expect.poll(() => newSession.getAttribute('data-shortcut-inline')).toBe(fits ? 'true' : null) + const centerOffset = () => newSession.evaluate((element) => { + const button = element.getBoundingClientRect() + const content = element.firstElementChild!.getBoundingClientRect() + return Math.abs(button.x + button.width / 2 - content.x - content.width / 2) + }) + expect(await centerOffset()).toBeLessThan(1) + await newSession.hover() + expect(await centerOffset()).toBeLessThan(1) + if (fits) { + expect(await newSession.locator('kbd').first().evaluate(element => getComputedStyle(element.parentElement!.parentElement!).opacity)).toBe('1') + expect(await page.getByRole('tooltip').count()).toBe(0) + } else { + await page.getByRole('tooltip', { name: keys, exact: true }).waitFor() + expect(await page.getByRole('tooltip').locator('kbd').allTextContents()).toEqual(keys.split(' ')) + } + await page.mouse.move(700, 100) + } + } finally { await context.close() } + }) + it.each([ { platform: 'Win32', modifier: 'Control', defaultBinding: 'Control+Alt+B' }, { platform: 'MacIntel', modifier: 'Meta', defaultBinding: 'Alt+Meta+B' }, @@ -369,7 +411,8 @@ describe('web e2e: shortcut reference', () => { await other.goto(scaffold.authenticatedUrl, { waitUntil: 'load' }) await other.getByRole('button', { name: 'Settings', exact: true }).waitFor() await other.keyboard.press(referenceKey) - await other.getByRole('button', { name: 'Remove shortcut for Toggle left sidebar', exact: true }).click() + await other.getByRole('button', { name: 'Edit shortcut for Toggle left sidebar', exact: true }).click() + await other.getByRole('button', { name: 'Remove', exact: true }).click() await editor.getByText('Shortcut configuration or available commands changed. Review the latest bindings before saving.', { exact: true }).waitFor() await page.bringToFront() await editor.getByRole('button', { name: 'Press a shortcut', exact: true }).focus() diff --git a/apps/web/tests/sidebar-right.e2e.ts b/apps/web/tests/sidebar-right.e2e.ts index e3c2a34c87..b389687677 100644 --- a/apps/web/tests/sidebar-right.e2e.ts +++ b/apps/web/tests/sidebar-right.e2e.ts @@ -1176,8 +1176,9 @@ describe('web e2e: shipped right Sidebar', () => { } expect(await terminalEntry.evaluate(node => getComputedStyle(node).overflow)).toBe('hidden') const terminalActions = terminalEntry.getByRole('button') - for (const action of await terminalActions.all()) { - expect(await action.evaluate(node => getComputedStyle(node).borderRadius)).toBe('0px') + const terminalButtons = await terminalActions.all() + for (const [index, action] of terminalButtons.entries()) { + expect(await action.evaluate(node => getComputedStyle(node).borderRadius)).toBe(index === 0 ? '0px' : '4px') await action.hover() expect(await action.evaluate(node => getComputedStyle(node).backgroundColor)).not.toBe('rgba(0, 0, 0, 0)') } diff --git a/packages/client/ui-conversation/README.i18n.yaml b/packages/client/ui-conversation/README.i18n.yaml index 8b219abf38..2f8802d3f4 100644 --- a/packages/client/ui-conversation/README.i18n.yaml +++ b/packages/client/ui-conversation/README.i18n.yaml @@ -18,8 +18,8 @@ en: 13ae68865fa315d7 zh: c152f020abf20f35 /deepseek-ai-dsh-client-ui-conversation/shell-and-standard-props: - en: 8de4fbd9df016e06 - zh: 06b15ab7855120cb + en: 0e54fdecaa1b2139 + zh: 1e9aca1712811d3b /deepseek-ai-dsh-client-ui-conversation/temporary-composer-entries: en: bb1244310f5c0382 zh: 824b84077432b559 diff --git a/packages/client/ui-conversation/README.md b/packages/client/ui-conversation/README.md index 7c7739fee9..dbf1c8a965 100644 --- a/packages/client/ui-conversation/README.md +++ b/packages/client/ui-conversation/README.md @@ -84,7 +84,7 @@ File chips and editable skill references share a whole-reference hover backgroun When another writer owns the Session, the send-error toast asks the user to quit other running DSH instances and retry. -Two independent Escape presses in the focused Chat or Composer stop its current running turn and preserve queued messages. The interval comes from the shortcuts plugin’s `stopSequenceMs` configuration (500 ms by default). A menu, approval, modal, terminal, embedded webpage, composition, repeated key, changed input region, Session or turn breaks the sequence. The shortcut uses the same scoped cancellation as the Stop button. The plugin registers Stop as a fixed action in the `application` display group. Its registration reserves plain Escape against editable shortcuts and supplies the `Esc Esc` sequence shown in the Stop button’s hover and keyboard-focus tooltip. +Two independent Escape presses in the focused Chat or Composer stop its current running turn and preserve queued messages. The interval comes from the shortcuts plugin’s `stopSequenceMs` configuration (500 ms by default). A menu, approval, modal, terminal, embedded webpage, composition, repeated key, changed input region, Session or turn breaks the sequence. The shortcut uses the same scoped cancellation as the Stop button. The plugin registers Stop as a fixed action in the `input` display group. Its registration reserves plain Escape against editable shortcuts and supplies the `Esc Esc` sequence shown in the Stop button’s hover and keyboard-focus tooltip. ## Temporary composer entries diff --git a/packages/client/ui-conversation/README.zh.md b/packages/client/ui-conversation/README.zh.md index 88ac5cb99f..1aee4d23d4 100644 --- a/packages/client/ui-conversation/README.zh.md +++ b/packages/client/ui-conversation/README.zh.md @@ -84,7 +84,7 @@ Send 和 Stop 按钮禁用时不显示提示气泡,轮次结束后由 Stop 切 当会话被其他写句柄占用时,发送失败的 toast 提示用户退出其他正在运行的 DSH 后重试。 -在获得焦点的 Chat 或 Composer 中连续独立按下两次 Esc,可停止当前运行轮次并保留排队消息。间隔由 shortcuts 插件的 `stopSequenceMs` 配置决定,默认 500 ms。菜单、审批、模态层、终端、内嵌网页、输入法、重复按键,以及输入区域、Session 或轮次变化会清空序列。快捷键与 Stop 按钮调用同一作用域取消操作。插件将 Stop 注册为 `application` 展示分组中的固定操作。该注册使普通 Esc 不能分配给可编辑快捷键,并为 Stop 按钮的悬停和键盘聚焦提示提供 `Esc Esc` 序列。 +在获得焦点的 Chat 或 Composer 中连续独立按下两次 Esc,可停止当前运行轮次并保留排队消息。间隔由 shortcuts 插件的 `stopSequenceMs` 配置决定,默认 500 ms。菜单、审批、模态层、终端、内嵌网页、输入法、重复按键,以及输入区域、Session 或轮次变化会清空序列。快捷键与 Stop 按钮调用同一作用域取消操作。插件将 Stop 注册为 `input` 展示分组中的固定操作。该注册使普通 Esc 不能分配给可编辑快捷键,并为 Stop 按钮的悬停和键盘聚焦提示提供 `Esc Esc` 序列。 ## 临时 composer entry diff --git a/packages/client/ui-conversation/src/client/apply.ts b/packages/client/ui-conversation/src/client/apply.ts index 550515deda..942444d3b7 100644 --- a/packages/client/ui-conversation/src/client/apply.ts +++ b/packages/client/ui-conversation/src/client/apply.ts @@ -256,7 +256,7 @@ export function apply(ctx: Context, config: Config = Config({})): void { ), 'ui-conversation: fixed stop input') scope.effect(() => { const command: ShortcutFixedCommand = { - id: 'response.stop' as ShortcutCommandId, label: () => t('input.stop'), keys: ['Esc', 'Esc'], bindings: [{ code: 'Escape', modifiers: [] }], group: 'application', + id: 'response.stop' as ShortcutCommandId, label: () => t('input.stop'), keys: ['Esc', 'Esc'], bindings: [{ code: 'Escape', modifiers: [] }], group: 'input', } const dispose = scope.shortcuts.registerFixed(command) stopShortcut.set(command.keys) diff --git a/packages/client/ui-conversation/src/client/skeleton/InputBar.tsx b/packages/client/ui-conversation/src/client/skeleton/InputBar.tsx index 9139aa9ac8..f004aba1e9 100644 --- a/packages/client/ui-conversation/src/client/skeleton/InputBar.tsx +++ b/packages/client/ui-conversation/src/client/skeleton/InputBar.tsx @@ -311,7 +311,6 @@ export const InputBar = memo(function InputBar({ : running && steeringAvailable && !disabled && !uploadsPending && plainMessageDraft ? t(primarySubmitMode === 'steer' ? 'input.send.steer' : 'input.send.queue') : t('input.send') - const stopHint = stopKeys.length ? t('shortcut.hint', { label: t('input.stop'), keys: stopKeys.join(' ') }) : t('input.stop') const onPrimary = (): void => { if (primaryStops) { stop?.() @@ -459,7 +458,7 @@ export const InputBar = memo(function InputBar({ {renderSlot('conversation.input.activity', { locked, onActiveChange: setActivity })} } {interruptible && ( - + {(canCloseTab?.(tab.id) ?? true) && ( - + ) @@ -472,7 +472,7 @@ export function Menu({ open, anchor, items = [], children, selectedId, selectedI > {entry.icon !== undefined && {entry.icon}} {entry.label} - {entry.shortcut !== undefined && } + {entry.shortcut !== undefined && } {/* Selection marker is a trailing check (figma .Menu_cell) unless the fill mode carries it. */} {selected && selection === 'check' && } @@ -490,7 +490,7 @@ export function Menu({ open, anchor, items = [], children, selectedId, selectedI > {sub.icon !== undefined && {sub.icon}} {sub.label} - {sub.shortcut !== undefined && } + {sub.shortcut !== undefined && } ))} diff --git a/packages/client/ui-primitives/src/ShortcutKeys.module.css b/packages/client/ui-primitives/src/ShortcutKeys.module.css index 06d42e26dd..46b8e90cfe 100644 --- a/packages/client/ui-primitives/src/ShortcutKeys.module.css +++ b/packages/client/ui-primitives/src/ShortcutKeys.module.css @@ -1,16 +1,56 @@ .keys { + display: inline-flex; + flex: none; + align-items: center; + gap: 3px; + color: var(--dsw-alias-label-tertiary); + font-size: 12px; + line-height: 16px; + white-space: nowrap; +} + +.key { box-sizing: border-box; display: inline-flex; align-items: center; justify-content: center; - gap: 3px; - height: 26px; - padding: 6px 16px; - border-radius: 10px; + min-width: 20px; + height: 20px; + padding: 0 3px; + border: 0.5px solid var(--dsw-alias-border-l1); + border-radius: var(--dsw-radius-sm); background: var(--dsw-alias-bg-module-platform); - color: var(--dsw-alias-label-primary); - font-size: 12px; - line-height: 14px; - white-space: nowrap; + font: inherit; } -.key { font: inherit; } + +.plain .key { + min-width: 0; + height: auto; + padding: 0; + border: 0; + border-radius: 0; + background: transparent; +} + +.tooltip { color: inherit; font-size: 11px; line-height: 14px; gap: 2px; } +.tooltip .key { + min-width: 16px; + height: 16px; + padding: 0 2px; + border-radius: 4px; + border: 0; + background: var(--dsw-alias-tooltip-key-bg); +} + +.separator { font: inherit; } + +.joined { + box-sizing: border-box; + height: 20px; + padding: 0 6px; + border: 0.5px solid var(--dsw-alias-border-l1); + border-radius: var(--dsw-radius-sm); + background: var(--dsw-alias-bg-module-platform); +} +.joined .key { min-width: 0; height: auto; padding: 0; border: 0; background: transparent; } +.joined.tooltip { height: 16px; padding: 0 4px; border: 0; border-radius: 4px; background: var(--dsw-alias-tooltip-key-bg); } diff --git a/packages/client/ui-primitives/src/ShortcutKeys.tsx b/packages/client/ui-primitives/src/ShortcutKeys.tsx index 706c78e37e..e583015412 100644 --- a/packages/client/ui-primitives/src/ShortcutKeys.tsx +++ b/packages/client/ui-primitives/src/ShortcutKeys.tsx @@ -1,12 +1,12 @@ -/** Shared shortcut badges; callers supply the effective platform presentation. */ +/** Shared shortcut keycaps; callers supply the effective platform presentation. */ import css from './ShortcutKeys.module.css' import clsx from 'clsx' /** * Render one command's keycaps without owning binding defaults or localized copy. - * @param props - effective key labels and optional interaction styling. - * @returns one rounded badge containing the key sequence. + * @param props - effective key labels, presentation variant and optional interaction styling. + * @returns keycaps (joined for plus-separated combinations), or unboxed keys for compact text hints. */ -export function ShortcutKeys({ keys, className }: { keys: readonly string[]; className?: string | undefined }) { - return {keys.map((key, index) => {key})} +export function ShortcutKeys({ keys, variant = 'keycaps', className }: { keys: readonly string[]; variant?: 'keycaps' | 'plain' | 'tooltip'; className?: string | undefined }) { + return {keys.map((key, index) => {key})} } diff --git a/packages/client/ui-primitives/src/Tooltip.module.css b/packages/client/ui-primitives/src/Tooltip.module.css index 88de837e6a..db3d724528 100644 --- a/packages/client/ui-primitives/src/Tooltip.module.css +++ b/packages/client/ui-primitives/src/Tooltip.module.css @@ -1,4 +1,7 @@ .bubble { + display: inline-flex; + align-items: center; + gap: 8px; position: fixed; z-index: 100; /* Fixed-position shrink-to-fit measures only the space from `left` to the @@ -20,6 +23,12 @@ } .bubble[data-portal] { z-index: 1100; } +.label { min-width: 0; } + +/* A 16px keycap centered beside a 20px label has 5px of vertical clearance. */ +.bubble[data-has-shortcut] { padding-inline-end: 5px; } +.bubble[data-has-shortcut]:not(:has(.label)) { padding: 3px; } + .bubble[data-side='right'] { transform: translateY(-50%); diff --git a/packages/client/ui-primitives/src/Tooltip.tsx b/packages/client/ui-primitives/src/Tooltip.tsx index 8c2c8a819d..48cbf65e8f 100644 --- a/packages/client/ui-primitives/src/Tooltip.tsx +++ b/packages/client/ui-primitives/src/Tooltip.tsx @@ -3,6 +3,7 @@ import { cloneElement, createContext, useCallback, useContext, useEffect, useRef, useState } from 'react' import type { FocusEventHandler, MouseEventHandler, MutableRefObject, ReactElement, Ref } from 'react' import { createPortal } from 'react-dom' +import { ShortcutKeys } from './ShortcutKeys.tsx' import css from './Tooltip.module.css' /** Bubble placement relative to the anchor. */ @@ -41,6 +42,7 @@ if (typeof window !== 'undefined') { /** * Attach a hover/focus tooltip to an anchor element. * @param props.label - bubble text, or a resolver evaluated only while the bubble is visible. + * @param props.shortcutKeys - effective key labels rendered as platform-formatted keycaps after optional text. * @param props.side - placement relative to the anchor (default 'right'). * @param props.align - horizontal anchor-edge alignment for 'bottom'/'top' bubbles: 'end' pins * the bubble's right edge to the anchor's (for anchors beside other hover surfaces the centered @@ -60,7 +62,7 @@ if (typeof window !== 'undefined') { * anchor dismisses the bubble until the next trigger, and focus arriving after a pointer * interaction (a closing menu refocusing its trigger) never raises it. */ -export function Tooltip({ label, side = 'right', align = 'center', delayMs = 0, gap = 8, disabled = false, portal = false, maxWidth, children }: { label: TooltipLabel; side?: TooltipSide; align?: 'center' | 'end'; delayMs?: number; gap?: number; disabled?: boolean; portal?: boolean; maxWidth?: number; children: ReactElement }) { +export function Tooltip({ label, shortcutKeys, side = 'right', align = 'center', delayMs = 0, gap = 8, disabled = false, portal = false, maxWidth, children }: { label: TooltipLabel; shortcutKeys?: readonly string[] | undefined; side?: TooltipSide; align?: 'center' | 'end'; delayMs?: number; gap?: number; disabled?: boolean; portal?: boolean; maxWidth?: number; children: ReactElement }) { const anchor = useRef(null) // React 18 keeps the element's ref outside props; forward it so wrapping an // anchor in Tooltip never silently severs the owner's ref. @@ -191,10 +193,13 @@ export function Tooltip({ label, side = 'right', align = 'center', delayMs = 0, data-side={side} data-portal={portal || undefined} data-align={align} + data-has-shortcut={shortcutKeys?.length ? true : undefined} style={{ left: pos.x, top: y, visibility: 'hidden', ...maxWidth === undefined ? {} : { maxWidth } }} role="tooltip" + aria-label={shortcutKeys?.length ? [resolvedLabel, shortcutKeys.join(' ')].filter(Boolean).join(' ') : undefined} > - {resolvedLabel} + {resolvedLabel && {resolvedLabel}} + {shortcutKeys !== undefined && shortcutKeys.length > 0 && } ) diff --git a/packages/client/ui-primitives/src/plugin-artwork.tsx b/packages/client/ui-primitives/src/plugin-artwork.tsx index c5a774ca81..374d99e138 100644 --- a/packages/client/ui-primitives/src/plugin-artwork.tsx +++ b/packages/client/ui-primitives/src/plugin-artwork.tsx @@ -12,11 +12,11 @@ import type { IconProps } from './icons/props.ts' */ const useArtworkId = (): string => `dsh_plugin_art_${useId().replaceAll(':', '')}` -/** Terminal plugin artwork (prompt chevron and cursor bar). */ +/** Light-blue terminal artwork shared by plugin cards and sidebar guide entries. */ export const PluginArtworkTerminal = ({ size = 36, className }: IconProps) => ( - - + + ) diff --git a/packages/client/ui-primitives/tests/tooltip.client.spec.tsx b/packages/client/ui-primitives/tests/tooltip.client.spec.tsx index fb91da32a1..3d109a91bf 100644 --- a/packages/client/ui-primitives/tests/tooltip.client.spec.tsx +++ b/packages/client/ui-primitives/tests/tooltip.client.spec.tsx @@ -39,6 +39,18 @@ afterEach(() => { }) describe('Tooltip', () => { + it('updates independent keycaps and the accessible combination while visible', () => { + const view = render() + fireEvent.focus(screen.getByText('anchor')) + expect(Array.from(screen.getByRole('tooltip', { name: 'Reload ⌘ R' }).querySelectorAll('kbd'), key => key.textContent)).toEqual(['⌘', 'R']) + view.rerender() + expect(Array.from(screen.getByRole('tooltip', { name: 'Reload Ctrl + R' }).querySelectorAll('kbd'), key => key.textContent)).toEqual(['Ctrl', '+', 'R']) + view.rerender() + expect(screen.getByRole('tooltip').querySelector('kbd')).toBeNull() + fireEvent.click(screen.getByText('anchor')) + expect(screen.queryByRole('tooltip')).toBeNull() + }) + it('fits from observed sizes without synchronously measuring the bubble', () => { automaticResize = false const measured = vi.spyOn(Element.prototype, 'getBoundingClientRect') diff --git a/packages/client/ui-settings-general/src/client/SettingsRoot.tsx b/packages/client/ui-settings-general/src/client/SettingsRoot.tsx index af6cf8b40c..761ae2dc0f 100644 --- a/packages/client/ui-settings-general/src/client/SettingsRoot.tsx +++ b/packages/client/ui-settings-general/src/client/SettingsRoot.tsx @@ -218,7 +218,7 @@ export function SettingsRoot(props: SettingsRootComponentProps) { wide, settingsOpen: open, openSettings: actions.open, ...(shortcut?.keys.length ? { settingsShortcut: { keys: shortcut.keys, aria: shortcut.aria } } : {}), openOnboarding: (id) => { close(); setRequestedOnboarding(id) }, - }, { fallback: + }, { fallback: } {message || (desktopChords ? '' : t(runtime === 'web' diff --git a/packages/client/ui-shortcuts/src/client/Icons.tsx b/packages/client/ui-shortcuts/src/client/Icons.tsx index 5ba9c6778f..03dfcf7220 100644 --- a/packages/client/ui-shortcuts/src/client/Icons.tsx +++ b/packages/client/ui-shortcuts/src/client/Icons.tsx @@ -1,7 +1,6 @@ /** Supplied shortcut-control and feedback artwork, colored by the owning UI. */ const artwork = { edit: <>, - trash: <>, error: <>, success: <>, } @@ -12,6 +11,6 @@ const artwork = { * @returns an ornamental SVG hidden from assistive technology. */ export function ShortcutIcon({ kind, className }: { kind: keyof typeof artwork; className?: string | undefined }) { - const size = kind === 'edit' || kind === 'trash' ? 12 : 16 + const size = kind === 'edit' ? 12 : 16 return } diff --git a/packages/client/ui-shortcuts/src/client/Reference.module.css b/packages/client/ui-shortcuts/src/client/Reference.module.css index 919baae556..f1fa274d1b 100644 --- a/packages/client/ui-shortcuts/src/client/Reference.module.css +++ b/packages/client/ui-shortcuts/src/client/Reference.module.css @@ -61,6 +61,7 @@ box-shadow: 0 0 1px rgba(0, 0, 0, 0.2), 0 0 4px rgba(0, 0, 0, 0.02), 0 12px 32px rgba(0, 0, 0, 0.08); } .dialog:focus { outline: none; } +.contents { display: flex; flex-direction: column; flex: 1; min-height: 0; } .header { position: relative; flex: 0 0 46px; @@ -92,10 +93,11 @@ } .close:hover { background: var(--dsw-alias-interactive-bg-hover); } .searchRow { - flex: 0 0 72px; + box-sizing: border-box; + flex: 0 0 60px; display: flex; align-items: center; - padding: 0 24px; + padding: 14px 20px 10px; } .searchField { box-sizing: border-box; @@ -130,7 +132,7 @@ min-height: 0; overflow-y: auto; overscroll-behavior: contain; - padding: 0 24px 18px; + padding: 0 16px 18px; } .footer { flex: none; @@ -138,7 +140,7 @@ align-items: center; justify-content: space-between; gap: 12px; - min-height: 60px; + min-height: 52px; padding: 0 24px; border-top: 0.5px solid var(--dsw-alias-border-l2); font-size: 12px; @@ -152,16 +154,16 @@ border: 0; border-radius: 4px; background: transparent; - color: var(--dsw-alias-label-primary); + color: var(--dsw-alias-label-secondary); font: inherit; cursor: pointer; } -.resetAll:enabled:hover { color: var(--dsw-alias-label-secondary); } +.resetAll:enabled:hover { color: var(--dsw-alias-label-tertiary); } .resetAll:focus-visible { outline: 1px solid var(--dsw-alias-label-tertiary); outline-offset: 2px; } .resetAll:disabled { cursor: default; opacity: 0.45; } .modifiedCount { color: var(--dsw-alias-label-tertiary); } .group { - margin: 18px 0 4px; + margin: 18px 8px 4px; color: var(--dsw-alias-label-tertiary); font-size: 12px; line-height: 18px; @@ -169,32 +171,37 @@ } .rows { padding: 0; margin: 0; list-style: none; } .row { + position: relative; + border-radius: var(--dsw-radius-md); box-sizing: border-box; min-height: 42px; - padding: 8px 0; + padding: 8px; color: var(--dsw-alias-label-primary); font-size: 13px; line-height: 18px; } -.binding { flex-shrink: 0; } -.commandLabel { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.binding { position: relative; flex-shrink: 0; pointer-events: none; } +.binding:has(.inlineEditor) { pointer-events: auto; } +.commandLabel { position: relative; pointer-events: none; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .hint { color: var(--dsw-alias-label-tertiary); font-size: 12px; line-height: 18px; } p.hint { margin: 6px 0; } /* Keyboard focus reveals actions without retaining a mouse interaction's hover state. */ -.rowActions { display: inline-flex; align-items: center; gap: 8px; opacity: 0; } +.rowActions { display: inline-flex; align-items: center; justify-content: center; width: 20px; height: 26px; color: var(--dsw-alias-label-tertiary); opacity: 0; } .row:hover .rowActions, .row:has(:focus-visible) .rowActions { opacity: 1; } -.iconButton { display: inline-grid; place-items: center; width: 20px; height: 26px; border: 0; padding: 0; background: transparent; color: var(--dsw-alias-label-tertiary); cursor: pointer; } -.iconButton:focus-visible, .inlineAction:focus-visible { outline: 1px solid var(--dsw-alias-label-tertiary); outline-offset: 2px; border-radius: 4px; } -.keyButton { display: inline-flex; padding: 0; border: 0; border-radius: 10px; background: transparent; cursor: pointer; } -.keyButton:focus-visible { outline: 1px solid var(--dsw-alias-label-tertiary); outline-offset: 2px; } -.keyButton:enabled:hover .keyBadge, .keyButton:enabled:hover .unbound { background: var(--dsw-alias-interactive-bg-hover-accent); } +.inlineAction:focus-visible { outline: 1px solid var(--dsw-alias-label-tertiary); outline-offset: 2px; border-radius: 4px; } +.rowButton { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; border-radius: inherit; background: transparent; cursor: pointer; } +.row:has(.rowButton:enabled):hover { background: var(--dsw-alias-interactive-bg-hover); } +.rowButton:focus-visible { outline: 1px solid var(--dsw-alias-label-tertiary); outline-offset: -1px; } +.binding .keyBadge { color: var(--dsw-alias-label-secondary); } +.binding .fixedKeyBadge { height: 26px; color: var(--dsw-alias-label-tertiary); } +.keyBadge { box-sizing: border-box; justify-content: center; height: 26px; padding: 0 12px; border-radius: 10px; background: var(--dsw-alias-bg-module-platform); } .unbound { display: inline-flex; align-items: center; justify-content: center; box-sizing: border-box; height: 26px; padding: 6px 16px; border-radius: 10px; background: var(--dsw-alias-bg-module-platform); color: var(--dsw-alias-label-tertiary); font-size: 12px; line-height: 14px; white-space: nowrap; } .inlineEditor { display: flex; flex-direction: column; align-items: flex-end; gap: 6px; } .binding:has(.inlineEditor) { max-width: 100%; } .inlineControls { display: flex; justify-content: flex-end; align-items: center; gap: 8px; } .inlineAction { padding: 0; border: 0; background: transparent; color: var(--dsw-alias-label-tertiary); font: inherit; font-size: 12px; line-height: 18px; white-space: nowrap; cursor: pointer; } -.recorder { display: inline-flex; align-items: center; justify-content: center; box-sizing: border-box; min-width: 96px; height: 26px; padding: 3px 14px; border: 1px solid var(--dsw-alias-label-tertiary); border-radius: 10px; background: var(--dsw-alias-bg-module-platform); color: var(--dsw-alias-label-tertiary); font: inherit; font-size: 12px; line-height: 18px; white-space: nowrap; cursor: pointer; } +.recorder { display: inline-flex; align-items: center; justify-content: center; box-sizing: border-box; min-width: 96px; height: 26px; padding: 3px 14px; border: 0.5px solid var(--dsw-alias-border-l4); border-radius: 10px; background: var(--dsw-alias-bg-module-platform); color: var(--dsw-alias-label-secondary); font: inherit; font-size: 12px; line-height: 18px; white-space: nowrap; cursor: pointer; } .recorder:focus-visible { outline: 1px solid var(--dsw-alias-label-tertiary); outline-offset: 2px; } .recorder.invalid { border: 1px solid var(--dsw-alias-state-error-primary); box-shadow: none; } .recorded { height: auto; padding: 0; background: transparent; color: inherit; } @@ -203,4 +210,9 @@ p.hint { margin: 6px 0; } .toastError { color: var(--dsw-alias-state-warn-secondary); } .srOnly { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; } @media (hover: none) { .rowActions { opacity: 1; } } -.button:disabled, .iconButton:disabled, .keyButton:disabled, .inlineAction:disabled, .recorder[aria-disabled="true"] { cursor: default; opacity: 0.45; } +.button:disabled, .rowButton:disabled, .inlineAction:disabled, .recorder[aria-disabled="true"] { cursor: default; opacity: 0.45; } + +.search::-webkit-search-cancel-button { -webkit-appearance: none; } +.clearSearch { box-sizing: border-box; display: inline-flex; align-items: center; justify-content: center; flex: none; width: 16px; height: 16px; padding: 0; border: 0.5px solid var(--dsw-alias-label-tertiary); border-radius: 50%; corner-shape: round; background: transparent; color: var(--dsw-alias-label-secondary); cursor: pointer; } +.clearSearch:hover { background: var(--dsw-alias-interactive-bg-hover); } +.clearSearch:focus-visible { outline: 1px solid var(--dsw-alias-label-tertiary); outline-offset: 2px; } diff --git a/packages/client/ui-shortcuts/src/client/Reference.tsx b/packages/client/ui-shortcuts/src/client/Reference.tsx index 10ff58ccdb..6b841d34c2 100644 --- a/packages/client/ui-shortcuts/src/client/Reference.tsx +++ b/packages/client/ui-shortcuts/src/client/Reference.tsx @@ -42,7 +42,6 @@ const coreActionOrder = new Map([ 'pane.fullscreen.toggle', 'page.refresh', 'page.close', - 'response.stop', ].map((id, index) => [id, index])) /** @@ -57,7 +56,7 @@ export function ShortcutsRow({ actions, t, useCatalog }: PropsRuntime<'settings.
{t('settings')}

{t('description')}

- + @@ -114,11 +113,6 @@ export function ShortcutReference({ if (mounted.current) setBusy(false) return result } - const remove = async (row: ShortcutCatalogEntry): Promise => { - const result = await persist({ type: 'set', id: row.id, binding: null }, config.revision) - if (!mounted.current) return - notify(result.status === 'saved' ? t('saved') : shortcutFailure(result, catalog, t, runtime), result.status !== 'saved') - } const resetAll = async (): Promise => { /* v8 ignore next -- The reset control is accessible only while its confirmation is open. */ if (resetRevision === null) return @@ -143,7 +137,8 @@ export function ShortcutReference({ group: 'application' as const, })), ...fixedCatalog.map(row => ({ ...row, names: [row.id, row.keys.join(' ')] })), - ].sort((left, right) => (coreActionOrder.get(left.id) ?? coreActionOrder.size) - (coreActionOrder.get(right.id) ?? coreActionOrder.size) + ].sort((left, right) => Number(left.id === 'response.stop') - Number(right.id === 'response.stop') + || (coreActionOrder.get(left.id) ?? coreActionOrder.size) - (coreActionOrder.get(right.id) ?? coreActionOrder.size) || Number(left.id > right.id) - Number(left.id < right.id)) const ranked = rankByName(entries.flatMap(row => row.names.map(name => ({ name, label: row.label, row }))), query.trim()) const matches = [...new Set(ranked.map(match => match.row))] @@ -156,65 +151,63 @@ export function ShortcutReference({ }, [open, resetRevision, modifiedCount]) return <> -
-

{t('title')}

- -
-
-
- - { actions.search(event.target.value) }} /> +
{ + if (target === null || busy || !(event.target instanceof Element) || event.target.closest('button, input, a, [contenteditable="true"]') !== null) return + event.preventDefault() + closeEditor() + }}> +
+

{t('title')}

+ +
+
+
+ + { actions.search(event.target.value) }} /> + {query !== '' && } +
+
+ {(['application', 'input', 'menus', 'approval'] as const).map((group) => { + const rows = matches.filter(row => row.group === group) + if (rows.length === 0) return null + return
+ {group !== 'application' &&

{t(group)}

} +
    {rows.map(row =>
  • + {'modified' in row && target?.id !== row.id &&
  • )}
+
+ })} + {matches.length === 0 &&

{t('empty')}

} +
+
+ + {modifiedCount > 0 && {t('modified-count', { count: modifiedCount })}} +
-
- {(['application', 'input', 'menus', 'approval'] as const).map((group) => { - const rows = matches.filter(row => row.group === group) - if (rows.length === 0) return null - return
- {group !== 'application' &&

{t(group)}

} -
    {rows.map(row =>
  • - {row.label} - - {'modified' in row - ? target?.id === row.id - ? - : <> - - {row.binding !== null && } - - - - - : } - -
  • )}
-
- })} - {matches.length === 0 &&

{t('empty')}

} -
-
- - {t('modified-count', { count: modifiedCount })} -
{ if (!busy) setResetRevision(null) }} diff --git a/packages/client/ui-shortcuts/src/client/locales.ts b/packages/client/ui-shortcuts/src/client/locales.ts index 681228d735..84ea32f946 100644 --- a/packages/client/ui-shortcuts/src/client/locales.ts +++ b/packages/client/ui-shortcuts/src/client/locales.ts @@ -1,6 +1,6 @@ /** Shortcut reference copy; only implemented operations appear in the catalog. */ export const zh = { - 'edit-label': '修改{command}快捷键', 'remove-label': '移除{command}快捷键', + 'edit-label': '修改{command}快捷键', record: '按下快捷键', 'record-help': '松开组合键即保存。Tab 切换操作,Esc 退出录键。', 'web-help': '浏览器可用组合:Mod+/、Mod+Shift+,、Mod+Shift+.。Mod 在 Mac 上为 Command,其他系统为 Ctrl。', 'unsupported-key': '暂不支持这个按键。', reserved: '此组合由系统或文本编辑操作保留。', @@ -26,16 +26,17 @@ export const zh = { 'web-reload': '刷新页面', 'desktop-reload': '重新启动 Harness', 'using-defaults': '当前使用默认键位。', 'using-accepted': '当前继续使用上次读取成功的键位。', 'native-failed': '无法启用桌面录键保护,请关闭后重试。', + 'global-hint': '全局唤起查看', 'clear-search': '清空搜索', hint: '{label} {keys}', - title: '快捷键', open: '快捷键速查', settings: '快捷键', view: '查看快捷键', - description: '查看当前可用的快捷键和输入操作', search: '搜索快捷键', close: '关闭快捷键', + title: '快捷键', open: '快捷键速查', settings: '快捷键', view: '编辑快捷键', + description: '查看和编辑当前可用的快捷键和输入操作', search: '搜索快捷键', close: '关闭快捷键', application: '应用操作', input: '消息输入', menus: '菜单与弹层', approval: '审批区域', unbound: '暂无快捷键', empty: '没有匹配的快捷键', move: '移动菜单选择', select: '选择菜单项', dismiss: '关闭菜单或顶层弹窗', } /** Typed English counterpart. */ export const en: Record = { - 'edit-label': 'Edit shortcut for {command}', 'remove-label': 'Remove shortcut for {command}', + 'edit-label': 'Edit shortcut for {command}', record: 'Press a shortcut', 'record-help': 'Release the keys to save. Tab moves between actions; Esc cancels.', 'web-help': 'Browser combinations: Mod+/, Mod+Shift+,, Mod+Shift+.. Mod is Command on Mac and Ctrl elsewhere.', 'unsupported-key': 'This key is not supported.', reserved: 'This combination is reserved for system or text editing actions.', @@ -61,9 +62,10 @@ export const en: Record = { 'web-reload': 'reload the page', 'desktop-reload': 'restart Harness', 'using-defaults': 'Default bindings are active.', 'using-accepted': 'The last successfully read bindings remain active.', 'native-failed': 'Could not protect desktop key recording. Exit recording and try again.', + 'global-hint': 'Open from anywhere', 'clear-search': 'Clear search', hint: '{label} {keys}', - title: 'Keyboard shortcuts', open: 'Open keyboard shortcuts', settings: 'Keyboard shortcuts', view: 'View shortcuts', - description: 'Browse available shortcuts and input actions', search: 'Search shortcuts', close: 'Close keyboard shortcuts', + title: 'Keyboard shortcuts', open: 'Open keyboard shortcuts', settings: 'Keyboard shortcuts', view: 'Edit shortcuts', + description: 'View and edit available shortcuts and input actions', search: 'Search shortcuts', close: 'Close keyboard shortcuts', application: 'Application', input: 'Message input', menus: 'Menus and dialogs', approval: 'Approval area', unbound: 'No shortcut', empty: 'No matching shortcuts', move: 'Move menu selection', select: 'Select menu item', dismiss: 'Close menu or top dialog', diff --git a/packages/client/ui-shortcuts/tests/commands.client.spec.ts b/packages/client/ui-shortcuts/tests/commands.client.spec.ts index a58aa1ac3d..5b1401a75a 100644 --- a/packages/client/ui-shortcuts/tests/commands.client.spec.ts +++ b/packages/client/ui-shortcuts/tests/commands.client.spec.ts @@ -40,7 +40,7 @@ describe('assembled shortcut command owners', () => { const fixedIds = () => shortcuts.fixedCatalog.getSnapshot().map(row => row.id) expect(fixedIds()).toEqual(expect.arrayContaining(conversationIds)) expect(shortcuts.fixedCatalog.getSnapshot().find(row => row.id === 'response.stop')) - .toMatchObject({ group: 'application', keys: ['Esc', 'Esc'] }) + .toMatchObject({ group: 'input', keys: ['Esc', 'Esc'] }) await client.unload('@deepseek-ai/dsh-client-ui-shortcuts') expect(fixedIds()).toEqual(expect.arrayContaining(conversationIds)) expect(fixedIds()).not.toContain('fixed.move') diff --git a/packages/client/ui-shortcuts/tests/reference.client.spec.tsx b/packages/client/ui-shortcuts/tests/reference.client.spec.tsx index c3b37ef23f..78584a6ca8 100644 --- a/packages/client/ui-shortcuts/tests/reference.client.spec.tsx +++ b/packages/client/ui-shortcuts/tests/reference.client.spec.tsx @@ -38,7 +38,8 @@ it('keeps a reference open while a clear operation is pending', async () => { const f = referenceFixture() const pending = Promise.withResolvers>>() f.edit.mockReturnValueOnce(pending.promise) - fireEvent.click(screen.getByRole('button', { name: 'Remove shortcut for Open settings' })) + fireEvent.click(screen.getByRole('button', { name: 'Edit shortcut for Open settings' })) + fireEvent.click(screen.getByRole('button', { name: en.clear })) fireEvent.click(screen.getByRole('button', { name: en.close })) expect(f.store.getSnapshot().open).toBe(true) await act(async () => { pending.resolve({ status: 'saved', snapshot: f.config.getSnapshot() }); await pending.promise }) @@ -70,7 +71,7 @@ it.each(['macos', 'windows'] as const)('shows the reference, filters labels and describeBinding: describe, platform, t: makeTranslate(en) } as Parameters[0] render(<>) expect(screen.queryByRole('dialog')).toBeNull() - const opener = screen.getByRole('button', { name: 'View shortcuts' }); opener.focus(); fireEvent.click(opener) + const opener = screen.getByRole('button', { name: en.view }); opener.focus(); fireEvent.click(opener) expect(opener.getAttribute('aria-keyshortcuts')).toBe(platform === 'macos' ? 'Meta+/' : 'Control+/') const search = screen.getByRole('searchbox') expect(document.activeElement).toBe(search) @@ -100,7 +101,7 @@ it.each(['macos', 'windows'] as const)('shows the reference, filters labels and expect(store.getSnapshot().query).toBe('') expect(document.activeElement).toBe(opener) fireEvent.mouseEnter(opener) - expect(screen.getByRole('tooltip').textContent).toBe(platform === 'macos' ? 'View shortcuts ⌘ /' : 'View shortcuts Ctrl + /') + expect(screen.getByRole('tooltip').getAttribute('aria-label')).toBe(`${en['global-hint']} ${platform === 'macos' ? '⌘ /' : 'Ctrl + /'}`) act(() => { catalog.set(catalog.getSnapshot().map(row => row.id === 'shortcuts.open' ? { ...row, binding: null, keys: [], aria: undefined } : row)) }) expect(screen.queryByRole('tooltip')).toBeNull() @@ -186,7 +187,7 @@ it('keeps core actions in product order across registration, remount and label c ...template, id: id as ShortcutCommandId, label, })) const stop: ShortcutFixedCatalogEntry = { id: 'response.stop' as ShortcutCommandId, label: 'Stop reply', - keys: ['Esc', 'Esc'], bindings: [{ code: 'Escape', modifiers: [] }], group: 'application' } + keys: ['Esc', 'Esc'], bindings: [{ code: 'Escape', modifiers: [] }], group: 'input' } const fixed: ShortcutFixedCatalogEntry[] = [ { ...stop, id: 'fixed.send' as ShortcutCommandId, label: 'Send', keys: ['Enter'], bindings: [{ code: 'Enter', modifiers: [] }], group: 'input' }, { ...stop, id: 'fixed.newline' as ShortcutCommandId, label: 'Newline', keys: ['Shift', 'Enter'], bindings: [{ code: 'Enter', modifiers: ['shift'] }], group: 'input' }, @@ -196,21 +197,21 @@ it('keeps core actions in product order across registration, remount and label c stop, ] const labels = (group: string) => within(screen.getByRole('region', { name: group })).getAllByRole('listitem') - .map(row => row.firstElementChild?.textContent) - const expected = [...core.map(([, label]) => label), 'Stop reply', 'Zulu extension', 'Alpha extension'] + .map(row => row.querySelector(':scope > span')?.textContent) + const expected = [...core.map(([, label]) => label), 'Zulu extension', 'Alpha extension'] for (const reversed of [true, false]) { act(() => { catalog.set(reversed ? [...extensions, ...commands].reverse() : [...extensions, ...commands]) fixedCatalog.set(reversed ? [...fixed].reverse() : fixed) }) expect(labels(en.application)).toEqual(expected) - expect(labels(en.input)).toEqual(['Newline', 'Send']) + expect(labels(en.input)).toEqual(['Newline', 'Send', 'Stop reply']) expect(labels(en.menus)).toEqual(['Dismiss', 'Select']) expect(labels(en.approval)).toEqual(['Approve']) } - const stopButton = within(screen.getByRole('region', { name: en.application })).getByRole('button', { name: 'Stop reply Esc Esc' }) - expect(stopButton.hasAttribute('disabled')).toBe(true) - fireEvent.click(stopButton) + const stopRow = screen.getByText('Stop reply').closest('li')! + expect(within(stopRow).queryByRole('button')).toBeNull() + fireEvent.click(stopRow) expect(screen.queryByRole('button', { name: 'Edit shortcut for Stop reply' })).toBeNull() expect(screen.queryByRole('group', { name: 'Stop reply' })).toBeNull() act(() => { catalog.set(extensions); fixedCatalog.set(fixed.filter(row => row.id !== stop.id)) }) @@ -223,7 +224,7 @@ it('keeps core actions in product order across registration, remount and label c store.actions.search('操作') }) expect(labels(en.application)).toEqual(expected.map(label => `操作:${label}`)) - expect(labels(en.input)).toEqual(['操作:Newline', '操作:Send']) + expect(labels(en.input)).toEqual(['操作:Newline', '操作:Send', '操作:Stop reply']) }) it('keeps product order for tied searches while prioritizing stronger matches', () => { @@ -268,9 +269,9 @@ it('shows mounted fixed actions as searchable read-only rows and follows their l const approve = { id: 'approval.accept' as ShortcutCommandId, label: 'Approve', keys: ['Enter'], bindings: [{ code: 'Enter', modifiers: [] }], group: 'approval' as const } expect(screen.queryByText(send.label)).toBeNull() act(() => { fixedCatalog.set([send, stop, approve]) }) - expect(screen.getByRole('button', { name: 'Send from catalog Enter' }).hasAttribute('disabled')).toBe(true) + expect(within(screen.getByText(send.label).closest('li')!).queryByRole('button')).toBeNull() expect(screen.getByRole('region', { name: 'Approval area' }).textContent).toContain('Approve') - expect(screen.getByRole('button', { name: 'Stop reply Esc Esc' }).hasAttribute('disabled')).toBe(true) + expect(within(screen.getByText(stop.label).closest('li')!).queryByRole('button')).toBeNull() expect(screen.queryByRole('button', { name: 'Edit shortcut for Stop reply' })).toBeNull() act(() => { store.actions.search('response.stop') }) expect(screen.getAllByRole('listitem')).toHaveLength(1) @@ -305,12 +306,13 @@ it.each(['conflict', 'reserved'] as const)('omits status labels for a %s binding expect(screen.getByRole('button', { name: 'Edit shortcut for Open settings' }).hasAttribute('disabled')).toBe(false) }) -it('removes from the row and replaces failure feedback with a fresh system success toast', async () => { +it('removes from the inline editor and replaces failure feedback with a fresh system success toast', async () => { vi.useFakeTimers() try { const f = referenceFixture() f.edit.mockResolvedValueOnce({ status: 'write-failed', snapshot: f.config.getSnapshot() }) - const remove = screen.getByRole('button', { name: 'Remove shortcut for Open settings' }) + fireEvent.click(screen.getByRole('button', { name: 'Edit shortcut for Open settings' })) + const remove = screen.getByRole('button', { name: en.clear }) await act(async () => { remove.click() }) expect(screen.getByRole('alert').textContent).toBe(en['write-failed']) expect(screen.getByRole('dialog').contains(screen.getByRole('alert'))).toBe(false) @@ -325,10 +327,10 @@ it('removes from the row and replaces failure feedback with a fresh system succe } finally { vi.useRealTimers() } }) -it('opens the same recorder from a key badge and retains the editor when restoring defaults fails', async () => { +it('opens the recorder from the application row and retains the editor when restoring defaults fails', async () => { const f = referenceFixture() f.edit.mockResolvedValueOnce({ status: 'write-failed', snapshot: f.config.getSnapshot() }) - fireEvent.click(screen.getByRole('button', { name: 'Open settings ⇧ ⌘ ,' })) + fireEvent.click(screen.getByRole('button', { name: 'Edit shortcut for Open settings' })) await act(async () => { screen.getByRole('button', { name: en.reset }).click() }) expect(f.edit).toHaveBeenLastCalledWith({ type: 'reset', id: 'settings.open' }, f.config.getSnapshot().revision) expect(screen.getByRole('alert').textContent).toBe(en['write-failed']) @@ -390,7 +392,8 @@ it('blocks reference dismissal during a removal and ignores its completion after let settle!: (value: Awaited>) => void const reply = new Promise>>((resolve) => { settle = resolve }) f.edit.mockReturnValueOnce(reply) - fireEvent.click(screen.getByRole('button', { name: 'Remove shortcut for Open settings' })) + fireEvent.click(screen.getByRole('button', { name: 'Edit shortcut for Open settings' })) + fireEvent.click(screen.getByRole('button', { name: en.clear })) fireEvent.keyDown(screen.getByRole('dialog'), { key: 'Escape' }) expect(f.store.getSnapshot().open).toBe(true) f.view.unmount() @@ -482,7 +485,7 @@ it('restores defaults after confirmation and retains accepted configuration when await act(async () => { confirm.click() }) expect(screen.queryByRole('dialog', { name: en['reset-title'] })).toBeNull() expect(screen.getByRole('alert').textContent).toBe(en['reset-saved']) - expect(screen.getByText('0 customized')).toBeTruthy() + expect(screen.queryByText('0 customized')).toBeNull() expect(restore.hasAttribute('disabled')).toBe(true) expect(document.activeElement).toBe(screen.getByRole('searchbox')) }) @@ -523,3 +526,34 @@ it('keeps an active recorder focused when another window resets the profile', () expect(document.activeElement).toBe(recorder) expect(screen.getByRole('group', { name: 'Open settings' })).toBeTruthy() }) + +it('clears a search and returns focus to the search field', () => { + referenceFixture() + const search = screen.getByRole('searchbox') + fireEvent.change(search, { target: { value: 'absent' } }) + expect(screen.queryAllByRole('listitem')).toHaveLength(0) + fireEvent.click(screen.getByRole('button', { name: en['clear-search'] })) + expect(screen.getAllByRole('listitem')).toHaveLength(1) + expect(document.activeElement).toBe(search) + expect(screen.queryByRole('button', { name: en['clear-search'] })).toBeNull() +}) + +it('closes the editor and drops a pending recording when blank space is clicked', async () => { + const f = referenceFixture() + fireEvent.click(screen.getByRole('button', { name: 'Edit shortcut for Open settings' })) + const recorder = screen.getByRole('button', { name: en.record }) + expect(document.activeElement).toBe(recorder) + fireEvent.keyDown(recorder, { key: '.', code: 'Period', metaKey: true, shiftKey: true }) + fireEvent.pointerDown(screen.getByRole('heading', { name: en.title })) + expect(document.activeElement).toBe(screen.getByRole('dialog')) + await act(async () => { fireEvent.keyUp(document.activeElement!, { key: '.', code: 'Period' }) }) + expect(f.edit).not.toHaveBeenCalled() + expect(screen.queryByRole('group', { name: 'Open settings' })).toBeNull() + fireEvent.click(screen.getByRole('button', { name: 'Edit shortcut for Open settings' })) + const reopened = screen.getByRole('button', { name: en.record }) + await act(async () => { + fireEvent.keyDown(reopened, { key: '.', code: 'Period', metaKey: true, shiftKey: true }) + fireEvent.keyUp(reopened, { key: '.', code: 'Period' }) + }) + expect(f.edit).toHaveBeenCalledOnce() +}) diff --git a/packages/client/ui-sidebar-browser/README.i18n.yaml b/packages/client/ui-sidebar-browser/README.i18n.yaml index 408861fa2f..4c5c2a8a75 100644 --- a/packages/client/ui-sidebar-browser/README.i18n.yaml +++ b/packages/client/ui-sidebar-browser/README.i18n.yaml @@ -21,8 +21,8 @@ en: fb79cb7d59bc01af zh: 5f6202d8b0b62d7d /deepseek-ai-dsh-client-ui-sidebar-browser/use-this-package/minimal-configuration: - en: fbbc95839824742c - zh: fb49613017959ff4 + en: 32511e10645b4154 + zh: ce0e0efd60dda336 /deepseek-ai-dsh-client-ui-sidebar-browser/understand-the-implementation: en: 42b15ac6321706aa zh: e15901d8462ec7c7 diff --git a/packages/client/ui-sidebar-browser/README.md b/packages/client/ui-sidebar-browser/README.md index c50036ed03..a9cc06b03d 100644 --- a/packages/client/ui-sidebar-browser/README.md +++ b/packages/client/ui-sidebar-browser/README.md @@ -42,7 +42,7 @@ The package has no plugin configuration. A Web profile enables the shipped entry Client plugins can open a tab through `ctx.sidebarRight.openTab('browser', { params: { url } })`. The optional URL passes the same validation as address-bar input before navigation. -The `browser.new` command opens a separate Browser page in the focused dock pane, replacing a guide and retaining existing content pages. From the conversation or a floating content page, it uses the active dock pane. Desktop defaults to Cmd+T on macOS and Ctrl+T on Windows; Windows and macOS Web use the [shortcut service’s platform defaults](../shortcuts/README.md); Linux Web leaves the command unbound. The guide button displays the effective shortcut inline without a duplicate tooltip. +The `browser.new` command opens a separate Browser page in the focused dock pane, replacing a guide and retaining existing content pages. From the conversation or a floating content page, it uses the active dock pane. Desktop defaults to Cmd+T on macOS and Ctrl+T on Windows; Windows and macOS Web use the [shortcut service’s platform defaults](../shortcuts/README.md); Linux Web leaves the command unbound. The guide button uses a blue globe and displays the effective shortcut inline without a duplicate tooltip. The toolbar provides Back, Forward, Reload, Go, and Open in system browser. Web also offers a per-tab sandbox toggle; disabling it is temporary and displays a warning. Desktop shows the observed page title. After a restart, Browser shows the saved title and URL; Restore or Reload opens that address only when requested. diff --git a/packages/client/ui-sidebar-browser/README.zh.md b/packages/client/ui-sidebar-browser/README.zh.md index ab79ca401a..32e9a43bf6 100644 --- a/packages/client/ui-sidebar-browser/README.zh.md +++ b/packages/client/ui-sidebar-browser/README.zh.md @@ -42,7 +42,7 @@ Browser 在 Web profile 中默认禁用,在 Desktop 中默认启用。Web 用 Client 插件可以调用 `ctx.sidebarRight.openTab('browser', { params: { url } })` 打开 tab。可选 URL 会在导航前接受与地址栏输入相同的校验。 -命令 `browser.new` 在焦点停靠分栏打开独立浏览器页,替换开始页并保留已有内容页。从聊天区或浮动内容页触发时,使用活动停靠分栏。桌面默认键在 macOS 上为 Cmd+T,在 Windows 上为 Ctrl+T;Windows 和 macOS Web 使用[快捷键服务的平台默认值](../shortcuts/README.zh.md);Linux Web 默认不绑定此命令。开始页按钮内显示有效快捷键,不额外弹出重复提示。 +命令 `browser.new` 在焦点停靠分栏打开独立浏览器页,替换开始页并保留已有内容页。从聊天区或浮动内容页触发时,使用活动停靠分栏。桌面默认键在 macOS 上为 Cmd+T,在 Windows 上为 Ctrl+T;Windows 和 macOS Web 使用[快捷键服务的平台默认值](../shortcuts/README.zh.md);Linux Web 默认不绑定此命令。开始页按钮使用蓝色地球图标,并在按钮内显示有效快捷键,不额外弹出重复提示。 工具栏提供后退、前进、刷新、前往和在系统浏览器中打开。Web 还提供逐 tab sandbox 开关;关闭它是临时选择,并会显示警告。Desktop 显示观察到的页面标题。重启后,Browser 展示保存的标题和 URL;只有点击恢复或刷新才打开该地址。 diff --git a/packages/client/ui-sidebar-browser/src/client/definition.tsx b/packages/client/ui-sidebar-browser/src/client/definition.tsx index a829e10533..5298f824b8 100644 --- a/packages/client/ui-sidebar-browser/src/client/definition.tsx +++ b/packages/client/ui-sidebar-browser/src/client/definition.tsx @@ -1,7 +1,7 @@ /** Static Browser tab type and guide declaration. */ import type { ShortcutCommandId } from '@deepseek-ai/dsh-client-shortcuts/client' import type { TranslateNS } from '@deepseek-ai/dsh-client-locale/client' -import { IconGlobeOutlineRegular } from '@deepseek-ai/dsh-client-ui-primitives' +import type { IconProps } from '@deepseek-ai/dsh-client-ui-primitives' import type { SidebarRightTabDefinition } from '@deepseek-ai/dsh-client-ui-sidebar-right/client' import type {} from './locales.ts' @@ -11,6 +11,18 @@ export const BROWSER_KIND = 'browser' /** Browser implementation identity and keyed Slot dispatch key. */ export const BROWSER_ID = '@deepseek-ai/dsh-client-ui-sidebar-browser' +/** Blue globe artwork for the guide entry. */ +function BrowserGuideIcon({ size = 36, className }: IconProps) { + return ( + + ) +} + /** Build the Browser type with locale-live copy. */ export function browserDefinition(t: TranslateNS<'sidebarBrowser'>): SidebarRightTabDefinition { return { @@ -21,7 +33,7 @@ export function browserDefinition(t: TranslateNS<'sidebarBrowser'>): SidebarRigh title: () => t('type.label'), guide: [{ id: 'new', commandId: 'browser.new' as ShortcutCommandId, order: 30, title: () => t('guide.title'), - description: () => t('guide.description'), icon: IconGlobeOutlineRegular, + description: () => t('guide.description'), icon: BrowserGuideIcon, }], } } diff --git a/packages/client/ui-sidebar-browser/src/client/view/BrowserBody.tsx b/packages/client/ui-sidebar-browser/src/client/view/BrowserBody.tsx index 3003c2f623..f819dba436 100644 --- a/packages/client/ui-sidebar-browser/src/client/view/BrowserBody.tsx +++ b/packages/client/ui-sidebar-browser/src/client/view/BrowserBody.tsx @@ -81,7 +81,7 @@ export function BrowserBody(props: BrowserBodyProps): ReactNode {
- +
diff --git a/packages/client/ui-sidebar-documentpreview/src/client/TextPreview.tsx b/packages/client/ui-sidebar-documentpreview/src/client/TextPreview.tsx index 65b517dbef..101f2debd8 100644 --- a/packages/client/ui-sidebar-documentpreview/src/client/TextPreview.tsx +++ b/packages/client/ui-sidebar-documentpreview/src/client/TextPreview.tsx @@ -310,7 +310,7 @@ export function TextPreview({ {/* Preview and file-tree refresh controls own different reload lifecycles and locale namespaces. */} {/* jscpd:ignore-start */} - + - + - + ) } diff --git a/packages/client/ui-sidebar-right/tests/expand-button.client.spec.tsx b/packages/client/ui-sidebar-right/tests/expand-button.client.spec.tsx index 555bbdf495..8a5a4a7bac 100644 --- a/packages/client/ui-sidebar-right/tests/expand-button.client.spec.tsx +++ b/packages/client/ui-sidebar-right/tests/expand-button.client.spec.tsx @@ -55,7 +55,9 @@ describe('ExpandButton', () => { binding: null, keys: ['Ctrl', 'B'], aria: 'Control+B', modified: true, conflicts: [], issue: null }]) expect(control()?.getAttribute('aria-keyshortcuts')).toBe('Control+B') fireEvent.mouseEnter(control()!) - expect((await view.findByRole('tooltip')).textContent).toBe('shortcut.hint') + const tooltip = await view.findByRole('tooltip') + expect(tooltip.textContent).toContain('chrome.expand') + expect(Array.from(tooltip.querySelectorAll('kbd'), key => key.textContent)).toEqual(['Ctrl', 'B']) cleanup() }) it('offers the way in while the session has no surface yet, and asks the panel to expand', () => { diff --git a/packages/client/ui-sidebar-right/tests/seat.client.spec.tsx b/packages/client/ui-sidebar-right/tests/seat.client.spec.tsx index cc11d1c863..8c692290c9 100644 --- a/packages/client/ui-sidebar-right/tests/seat.client.spec.tsx +++ b/packages/client/ui-sidebar-right/tests/seat.client.spec.tsx @@ -743,10 +743,10 @@ describe('slot-owned useTabInfo', () => { }) const anchor = element(h.view.container, '[data-dockkit-split-button]').parentElement! fireEvent.focus(anchor) - expect(document.querySelector('[role="tooltip"]')?.textContent).toBe('Two panes is the limit ⌘ \\') + expect(document.querySelector('[role="tooltip"]')?.getAttribute('aria-label')).toBe('Two panes is the limit ⌘ \\') act(() => { h.catalog.set([{ ...split, binding: { code: 'KeyG', modifiers: ['control'] }, keys: ['Ctrl', '+', 'G'], aria: 'Control+G' }]) }) - expect(document.querySelector('[role="tooltip"]')?.textContent).toBe('Two panes is the limit Ctrl + G') + expect(document.querySelector('[role="tooltip"]')?.getAttribute('aria-label')).toBe('Two panes is the limit Ctrl + G') act(() => { h.catalog.set([{ ...split, binding: null, keys: [], aria: undefined }]) }) expect(document.querySelector('[role="tooltip"]')?.textContent).toBe('Two panes is the limit') }) diff --git a/packages/client/ui-sidebar-terminal/README.i18n.yaml b/packages/client/ui-sidebar-terminal/README.i18n.yaml index dc9984bc87..14e566dbc8 100644 --- a/packages/client/ui-sidebar-terminal/README.i18n.yaml +++ b/packages/client/ui-sidebar-terminal/README.i18n.yaml @@ -15,11 +15,11 @@ en: d152484eb41ac6b4 zh: 1fa9a72d1ee045cc /deepseek-ai-dsh-client-ui-sidebar-terminal/use-this-package: - en: 8cba6f2ee8cc9324 - zh: 046c5b8ff551f2d1 + en: 3f9f4b5514fc5552 + zh: ce6d84a9cc1da71a /deepseek-ai-dsh-client-ui-sidebar-terminal/understand-the-implementation: - en: 5fd7c0f80571300a - zh: 55429315124d0488 + en: 005b5496578870d3 + zh: 9883a41eef2a18a5 /deepseek-ai-dsh-client-ui-sidebar-terminal/further-exploration: en: 9a4ba1f433d5fda2 zh: b2b58d3d144d4eba diff --git a/packages/client/ui-sidebar-terminal/README.md b/packages/client/ui-sidebar-terminal/README.md index 78c8b5256e..fb2097bea1 100644 --- a/packages/client/ui-sidebar-terminal/README.md +++ b/packages/client/ui-sidebar-terminal/README.md @@ -25,9 +25,9 @@ Choose an installed shell from the right sidebar's Start page to run commands in ## Use this package -The `terminal.new` command creates a separate terminal in the focused dock pane, replacing a guide and retaining existing content pages. From the conversation or a floating content page, it uses the active dock pane. The guide button displays the current effective shortcut inline without a duplicate tooltip. Windows and macOS Web use the [shortcut service’s platform defaults](../shortcuts/README.md); Linux Web leaves the command unbound by default. +The `terminal.new` command creates a separate terminal in the focused dock pane, replacing a guide and retaining existing content pages. From the conversation or a floating content page, it uses the active dock pane. The guide card right-aligns the current effective shortcut as unboxed text without a duplicate tooltip. Windows and macOS Web use the [shortcut service’s platform defaults](../shortcuts/README.md); Linux Web leaves the command unbound by default. -Open the right sidebar and click **New terminal** to open the remembered available shell immediately. Its adjacent arrow opens the installed-shell menu; selecting an item remembers it and opens that shell directly. Discovery runs when the menu opens and does not allocate a terminal. A failed lookup offers Retry in the menu. Use **New tab** to return to the guide and open another terminal. +Open the right sidebar and click **New terminal** to open the remembered available shell immediately. The separate arrow beside the title opens the installed-shell menu; selecting an item remembers it and opens that shell directly. Discovery runs when the menu opens and does not allocate a terminal. A failed lookup offers Retry in the menu. Use **New tab** to return to the guide and open another terminal. Double-click the terminal's tab title to rename it. **Take control** makes the current attachment writable when another page owns input. A temporary disconnect preserves the screen and offers **Reconnect**, without exposing transport diagnostics. An exited shell remains visible with its exit code and offers **New terminal**; it never restarts automatically. Exited terminals count toward the Session limit; close unused tabs when the limit is reached. @@ -43,7 +43,7 @@ The terminal background, default text, cursor, and selection follow the DSH them
Implementation internals — click to expand -This plugin registers the `terminal` type and body/title seats with the right sidebar. The guide uses a compact dark terminal card, while tab titles retain the line glyph. The React-free terminal model belongs to `api-terminal-controller`; keyed framework hooks expose its state. `ui-primitives` Menu and Button provide the shell picker and startup controls, including keyboard navigation and the selected-item marker. The body loads its package-local `client.terminal.js` chunk when a terminal view mounts, keeping xterm.js and FitAddon out of the startup `client.js`; they then render the screen and measure the viewport. The body reserves an 8px gap below the tab strip within the pane height. Input, including Tab and control characters, travels unchanged to the PTY. +This plugin registers the `terminal` type and body/title seats with the right sidebar. The guide reuses the Plugins page’s blue terminal artwork; tab titles use the line glyph. The React-free terminal model belongs to `api-terminal-controller`; keyed framework hooks expose its state. `ui-primitives` Menu and Button provide the shell picker and startup controls, including keyboard navigation and the selected-item marker. The body loads its package-local `client.terminal.js` chunk when a terminal view mounts, keeping xterm.js and FitAddon out of the startup `client.js`; they then render the screen and measure the viewport. The body reserves an 8px gap below the tab strip within the pane height. Input, including Tab and control characters, travels unchanged to the PTY. The terminal controller saves each globally unique content identity's Host association independently and owns content recovery; the sidebar owns layout persistence. A recovered view cannot allocate a replacement process. The sidebar's close handler schedules cleanup through the [terminal controller](../../api/terminal-controller/README.md#understand-the-implementation) and returns synchronously. Browser component cleanup and the tab's abort signal only detach browser work. diff --git a/packages/client/ui-sidebar-terminal/README.zh.md b/packages/client/ui-sidebar-terminal/README.zh.md index 0279ac78fe..13a26f821f 100644 --- a/packages/client/ui-sidebar-terminal/README.zh.md +++ b/packages/client/ui-sidebar-terminal/README.zh.md @@ -25,9 +25,9 @@ kind: "package-reference" ## 使用此包 -命令 `terminal.new` 在焦点停靠分栏创建独立终端,替换开始页并保留已有内容页。从聊天区或浮动内容页触发时,使用活动停靠分栏。开始页按钮内显示当前有效快捷键,不额外弹出重复提示。Windows 和 macOS Web 使用[快捷键服务的平台默认值](../shortcuts/README.zh.md);Linux Web 默认不绑定此命令。 +命令 `terminal.new` 在焦点停靠分栏创建独立终端,替换开始页并保留已有内容页。从聊天区或浮动内容页触发时,使用活动停靠分栏。开始页卡片右侧对齐显示当前有效快捷键的无背景文字,不额外弹出重复提示。Windows 和 macOS Web 使用[快捷键服务的平台默认值](../shortcuts/README.zh.md);Linux Web 默认不绑定此命令。 -展开右侧栏,点击**新建终端**即可直接打开上次选择且仍可用的 shell。旁边的箭头展开已安装 shell 菜单;选择后立即记住该项并打开终端。菜单展开时才查询 shell,不分配终端进程;查询失败可在菜单内重试。通过**新标签页**回到开始页并打开更多终端。 +展开右侧栏,点击**新建终端**即可直接打开上次选择且仍可用的 shell。标题旁的独立箭头展开已安装 shell 菜单;选择后立即记住该项并打开终端。菜单展开时才查询 shell,不分配终端进程;查询失败可在菜单内重试。通过**新标签页**回到开始页并打开更多终端。 双击终端标签标题可重命名。其他页面持有输入权时,点击 **接管输入** 使当前连接可写。暂时断线时保留屏幕并提供 **重新连接**,不展示传输内部错误。已退出的 shell 仍显示退出码并提供 **新建终端**,不会自动重启。已退出的终端仍计入 Session 配额;达到上限时请关闭不用的标签。 @@ -43,7 +43,7 @@ kind: "package-reference"
实现细节 -插件向右侧栏注册 `terminal` 类型及正文和标题 seat。开始页入口使用紧凑的黑底终端图标,页签标题保留线条图标。无 React 依赖的终端模型属于 `api-terminal-controller`,通过框架 keyed hooks 暴露状态。`ui-primitives` 的 Menu 与 Button 提供 shell 选择和启动控件,支持键盘导航与选中标记。正文在 terminal 视图挂载时加载包内 `client.terminal.js` chunk,使 xterm.js 与 FitAddon 不进入启动 `client.js`;加载后由它们负责屏幕渲染和视口测量。正文在面板高度内为页签条下方预留 8px 间距。输入原样传到 PTY,包括 Tab 和控制字符。 +插件向右侧栏注册 `terminal` 类型及正文和标题 seat。开始页入口复用插件页面的蓝色终端图标,页签标题使用线条图标。无 React 依赖的终端模型属于 `api-terminal-controller`,通过框架 keyed hooks 暴露状态。`ui-primitives` 的 Menu 与 Button 提供 shell 选择和启动控件,支持键盘导航与选中标记。正文在 terminal 视图挂载时加载包内 `client.terminal.js` chunk,使 xterm.js 与 FitAddon 不进入启动 `client.js`;加载后由它们负责屏幕渲染和视口测量。正文在面板高度内为页签条下方预留 8px 间距。输入原样传到 PTY,包括 Tab 和控制字符。 终端 controller 独立保存每个全局唯一内容身份与 Host 的关联,并负责内容恢复;侧栏负责布局持久化。恢复视图不能分配替代进程。侧栏关闭 handler 通过[终端 controller](../../api/terminal-controller/README.zh.md#understand-the-implementation)安排清理并同步返回。浏览器组件清理和 tab 的 abort signal 只停止浏览器工作。 diff --git a/packages/client/ui-sidebar-terminal/src/client/TerminalGuide.module.css b/packages/client/ui-sidebar-terminal/src/client/TerminalGuide.module.css index 74b1cddf83..5d13db7b4d 100644 --- a/packages/client/ui-sidebar-terminal/src/client/TerminalGuide.module.css +++ b/packages/client/ui-sidebar-terminal/src/client/TerminalGuide.module.css @@ -1,11 +1,12 @@ -/* The enclosing card clips both button fills to the guide-owned radius. */ -.entry { box-sizing: border-box; display: flex; align-items: stretch; width: 100%; overflow: hidden; border: 0.5px solid var(--dsw-alias-border-l3); border-radius: var(--dsl-guide-entry-radius); background: var(--dsw-alias-bg-layer-1); } -.mainAnchor { display: flex; flex: 1; min-width: 0; } -.main { flex: 1; justify-content: flex-start; gap: 14px; min-width: 0; height: auto; min-height: 56px; padding: 14px 20px; border-radius: 0; text-align: left; } -.icon { flex: none; } -.text { flex: 1; display: flex; flex-direction: column; gap: 3px; min-width: 0; } +.entry { position: relative; box-sizing: border-box; display: flex; align-items: center; gap: 14px; width: 100%; min-height: 56px; padding: 14px 20px; overflow: hidden; border: 0.5px solid var(--dsw-alias-border-l3); border-radius: var(--dsl-guide-entry-radius); background: var(--dsw-alias-bg-layer-1); } +.main { position: absolute; inset: 0; width: 100%; height: 100%; border-radius: 0; } +.main:focus-visible { outline-offset: -2px; } +.icon { position: relative; display: flex; align-items: center; justify-content: center; flex: none; width: 26px; height: 26px; color: var(--dsw-alias-label-secondary); pointer-events: none; } +.text { position: relative; flex: 1; display: flex; flex-direction: column; gap: 3px; min-width: 0; pointer-events: none; } +.titleRow { display: flex; align-items: center; min-width: 0; gap: 4px; } .title { overflow: hidden; color: var(--dsw-alias-label-primary); font-size: 14px; line-height: 1.4; white-space: nowrap; text-overflow: ellipsis; } -.description { overflow: hidden; color: var(--dsw-alias-label-caption); font-size: 13px; line-height: 1.4; white-space: nowrap; text-overflow: ellipsis; } -.trigger { align-self: stretch; flex: none; height: auto; width: 44px; padding: 0; border-radius: 0; } -.trigger svg { width: 18px; height: 18px; color: var(--dsw-alias-label-tertiary); } -.menu { display: flex; align-self: stretch; flex: none; } +.description { overflow: hidden; color: var(--dsw-alias-label-tertiary); font-size: 11px; line-height: 1.4; white-space: nowrap; text-overflow: ellipsis; } +.trigger { flex: none; height: 20px; width: 20px; padding: 0; border-radius: 4px; } +.trigger svg { width: 14px; height: 14px; color: var(--dsw-alias-label-tertiary); } +.menu { display: flex; flex: none; pointer-events: auto; } +.shortcut { position: relative; pointer-events: none; } diff --git a/packages/client/ui-sidebar-terminal/src/client/TerminalGuide.tsx b/packages/client/ui-sidebar-terminal/src/client/TerminalGuide.tsx index f70490e4bd..c9c06718a1 100644 --- a/packages/client/ui-sidebar-terminal/src/client/TerminalGuide.tsx +++ b/packages/client/ui-sidebar-terminal/src/client/TerminalGuide.tsx @@ -2,12 +2,11 @@ import { useEffect, useState, type ReactNode } from 'react' import type { ShortcutCatalogEntry } from '@deepseek-ai/dsh-client-shortcuts/client' import type { HostObservable } from '@deepseek-ai/dsh-client-ui-slots' -import { ShortcutKeys, Button, IconChevronDownOutlineRegular, Menu } from '@deepseek-ai/dsh-client-ui-primitives' +import { PluginArtworkTerminal, ShortcutKeys, Button, IconChevronDownOutlineRegular, Menu } from '@deepseek-ai/dsh-client-ui-primitives' import type { TerminalLaunchShells } from '@deepseek-ai/dsh-api-terminal-controller/client' import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots' import type {} from '@deepseek-ai/dsh-client-ui-sidebar-right/client' import type {} from './locales.ts' -import { TerminalGuideIcon } from './TerminalIcon.tsx' import css from './TerminalGuide.module.css' /** Provider-owned discovery and browser preference writes. */ @@ -26,7 +25,7 @@ type MenuState = { phase: 'loading' } | { phase: 'ready'; choices: TerminalLaunc /** * Open the remembered shell from the card or choose another shell from its menu. - * The launch button displays its current effective shortcut inline. + * The card displays its effective shortcut; the shell menu sits beside the title. * @param props - guide copy, enclosing tab actions and cancellable discovery. * @returns separate launch and menu buttons within one guide card. */ @@ -54,31 +53,31 @@ export function TerminalGuide({ ? [{ id: 'loading', label: t('shellLoading'), disabled: true }] : [{ id: 'error', label: t('failed', { message: state.message }), disabled: true }, { id: 'retry', label: t('retry') }] return
- - + } + /> + + {description !== undefined && } - { setOpen(false) }} - onSelect={(path) => { - if (state.phase === 'failed') { setState({ phase: 'loading' }); setAttempt(value => value + 1); return } - selectShell(path) - setOpen(false) - tab.actions.openTab('terminal', { replaceTab: true, params: { shellPath: path } }) - }} - anchor={} - /> + {shortcut !== undefined && shortcut.keys.length > 0 && }
} diff --git a/packages/client/ui-sidebar-terminal/src/client/TerminalIcon.tsx b/packages/client/ui-sidebar-terminal/src/client/TerminalIcon.tsx index 4c0fa3d11c..a875c26837 100644 --- a/packages/client/ui-sidebar-terminal/src/client/TerminalIcon.tsx +++ b/packages/client/ui-sidebar-terminal/src/client/TerminalIcon.tsx @@ -1,6 +1,5 @@ -/** Terminal glyphs for the sidebar guide and tab title. */ +/** Terminal glyph for sidebar tab titles. */ import type { ReactNode } from 'react' -import type { IconProps } from '@deepseek-ai/dsh-client-ui-primitives' /** * Render the tab title's terminal prompt in the surrounding text color. @@ -12,15 +11,3 @@ export function TerminalIcon(): ReactNode { } - -/** - * Render the guide's terminal card within the folder glyph's painted bounds. - * @param props - canvas size and layout class supplied by the guide. - * @returns a dark rounded terminal card with a white prompt. - */ -export function TerminalGuideIcon({ size = 26, className }: IconProps): ReactNode { - return -} diff --git a/packages/client/ui-sidebar-terminal/src/client/index.ts b/packages/client/ui-sidebar-terminal/src/client/index.ts index 7fe95a27c1..98ac69d4c9 100644 --- a/packages/client/ui-sidebar-terminal/src/client/index.ts +++ b/packages/client/ui-sidebar-terminal/src/client/index.ts @@ -10,7 +10,7 @@ import type {} from '@deepseek-ai/dsh-client-ui-renderer/client' import type {} from '@deepseek-ai/dsh-client-locale/client' import type {} from '@deepseek-ai/dsh-client-ui-session/client' import type {} from '@deepseek-ai/dsh-client-ui-theme/client' -import { TerminalGuideIcon } from './TerminalIcon.tsx' +import { PluginArtworkTerminal } from '@deepseek-ai/dsh-client-ui-primitives' import { TerminalGuide, type TerminalGuideInjected } from './TerminalGuide.tsx' import { LazyTerminalBody } from './LazyTerminalBody.tsx' import { TerminalTitle } from './TerminalTitle.tsx' @@ -70,7 +70,7 @@ export function apply(ctx: Context): void { ctx.effect(() => ctx.locale.register(namespace, { zh, en }), 'ui-sidebar-terminal.copy') ctx.effect(() => ctx.sidebarRightTabs.register({ id, kind: 'terminal', multiple: true, priority: 'builtin', title: () => t('title'), - guide: [{ id: 'new', order: 20, title: () => t('new'), description: () => t('description'), icon: TerminalGuideIcon }], + guide: [{ id: 'new', order: 20, title: () => t('new'), description: () => t('description'), icon: PluginArtworkTerminal }], }), 'ui-sidebar-terminal.type') ctx.effect(() => ctx.sidebarRight.registerCloseHandler('terminal', (sessionId, tab) => { ctx.webTerminals.close(sessionId, tab.id, tab.contentId, terminalId(sessionId, tab.id)) diff --git a/packages/client/ui-sidebar-terminal/tests/apply.client.spec.ts b/packages/client/ui-sidebar-terminal/tests/apply.client.spec.ts index 90f4a7965d..408be7c1e4 100644 --- a/packages/client/ui-sidebar-terminal/tests/apply.client.spec.ts +++ b/packages/client/ui-sidebar-terminal/tests/apply.client.spec.ts @@ -95,7 +95,7 @@ it('registers terminal views without recovery or cleanup slots, then releases co const Icon = definition.guide?.[0]?.icon if (Icon === undefined) throw new Error('Terminal guide icon was not registered') expect(renderToStaticMarkup(createElement(Icon, { size: 22 }))).toContain('width="22"') - expect(renderToStaticMarkup(createElement(Icon))).toContain('width="26"') + expect(renderToStaticMarkup(createElement(Icon))).toContain('width="36"') expect(definition.multiple).toBe(true) expect(h.dictionaries.get('sidebarTerminal')).toEqual({ en, zh }) expect(h.entries.map(entry => [entry.name, entry.component, entry.locale])).toEqual([ diff --git a/packages/client/ui-sidebar-terminal/tests/terminal-guide.client.spec.tsx b/packages/client/ui-sidebar-terminal/tests/terminal-guide.client.spec.tsx index 13b8057cf0..4d0ef3d022 100644 --- a/packages/client/ui-sidebar-terminal/tests/terminal-guide.client.spec.tsx +++ b/packages/client/ui-sidebar-terminal/tests/terminal-guide.client.spec.tsx @@ -72,12 +72,12 @@ it.each(['hover', 'focus'] as const)('keeps the terminal shortcut inline without if (trigger === 'hover') fireEvent.mouseEnter(button) else fireEvent.focus(button) expect(h.view.queryByRole('tooltip')).toBeNull() - expect(button.textContent).toContain('Ctrl`') + expect(Array.from(h.view.container.querySelectorAll('kbd'), key => key.textContent)).toEqual(['Ctrl', '`']) expect(button.getAttribute('aria-keyshortcuts')).toBe('Control+`') h.setShortcut({ keys: ['Alt', 'T'], aria: 'Alt+t' }) expect(h.view.queryByRole('tooltip')).toBeNull() - expect(button.textContent).toContain('AltT') + expect(Array.from(h.view.container.querySelectorAll('kbd'), key => key.textContent)).toEqual(['Alt', 'T']) expect(button.getAttribute('aria-keyshortcuts')).toBe('Alt+t') h.setShortcut({ keys: [], aria: undefined }) @@ -92,6 +92,7 @@ it('loads on demand, remembers a selected shell before opening, and cancels on m const h = mount() h.open() await h.view.findByRole('menuitem', { name: 'zsh' }) + expect(h.openTab).not.toHaveBeenCalled() const signal = h.loadShells.mock.calls[0]![0] h.selectShell.mockImplementation(() => { expect(h.openTab).not.toHaveBeenCalled() }) fireEvent.click(h.view.getByRole('menuitem', { name: 'bash' })) diff --git a/packages/client/ui-sidebar/README.i18n.yaml b/packages/client/ui-sidebar/README.i18n.yaml index ee4d7543ff..28fc9ec607 100644 --- a/packages/client/ui-sidebar/README.i18n.yaml +++ b/packages/client/ui-sidebar/README.i18n.yaml @@ -18,8 +18,8 @@ en: a970f0547496634f zh: 36ef9a85b26e19f1 /deepseek-ai-dsh-client-ui-sidebar/use-this-package/brand-and-new-session: - en: 6daf5bc805b6ad3d - zh: f759fde0f7ad821b + en: 501dc10cc01d214c + zh: a3fcb4ff6ca53f5f /deepseek-ai-dsh-client-ui-sidebar/use-this-package/global-panel-entries: en: 5d3300d3506254c5 zh: 541ae1ee1e10b921 diff --git a/packages/client/ui-sidebar/README.md b/packages/client/ui-sidebar/README.md index 2750ab706a..91c94002a9 100644 --- a/packages/client/ui-sidebar/README.md +++ b/packages/client/ui-sidebar/README.md @@ -29,7 +29,7 @@ The sidebar is the navigation shell: users see the brand, start new sessions, co ### Brand and New Session -The expanded brand row renders `sidebar.brand.mark` and `sidebar.brand.name` as independent single slots; the collapsed rail renders the same mark slot. Without occupants, the shell uses the fish mark and a localized local-build label. A complete build stacks a code badge below the label as `version[-commit][-dirty]`, using `DSH_CLIENT_VERSION`, the optional 7-character `DSH_CLIENT_COMMIT_HASH`, and `DSH_CLIENT_GIT_DIRTY=true`; missing version metadata omits the badge. New Session targets the explicit Workspace used by a scoped action, otherwise the current Session's Workspace, otherwise the most recently active Workspace; when none exists it clears into the blank New Session page. New Session controls show the command registry's effective binding in tooltips and `aria-keyshortcuts`, including the resident macOS header control when the sidebar is hidden. +The expanded brand row renders `sidebar.brand.mark` and `sidebar.brand.name` as independent single slots; the collapsed rail renders the same mark slot. Without occupants, the shell uses the fish mark and a localized local-build label. A complete build stacks a code badge below the label as `version[-commit][-dirty]`, using `DSH_CLIENT_VERSION`, the optional 7-character `DSH_CLIENT_COMMIT_HASH`, and `DSH_CLIENT_GIT_DIRTY=true`; missing version metadata omits the badge. New Session targets the explicit Workspace used by a scoped action, otherwise the current Session's Workspace, otherwise the most recently active Workspace; when none exists it clears into the blank New Session page. The expanded New Session button reveals the effective binding as trailing grey text on hover or keyboard focus, keeping the icon and label centered together. When the trailing space is too narrow, a key-only tooltip replaces the inline hint. Icon-only controls retain tooltips with platform-formatted keycaps, including the resident macOS header control when the sidebar is hidden. All controls expose `aria-keyshortcuts`. ### Global panel entries diff --git a/packages/client/ui-sidebar/README.zh.md b/packages/client/ui-sidebar/README.zh.md index 32364811ae..83fce45384 100644 --- a/packages/client/ui-sidebar/README.zh.md +++ b/packages/client/ui-sidebar/README.zh.md @@ -29,7 +29,7 @@ dsh Web 客户端的侧边栏让用户识别当前构建、启动新会话、将 ### 品牌与 New Session -展开的品牌行把 `sidebar.brand.mark` 与 `sidebar.brand.name` 渲染为两个独立的 single slot;收起轨道则渲染同一个 mark slot。没有占位者时,外壳使用鱼形标记和本地化的本地构建标签。完整构建会在标签下方显示代码徽标;该徽标使用 `DSH_CLIENT_VERSION`、可选的 7 位 `DSH_CLIENT_COMMIT_HASH` 与 `DSH_CLIENT_GIT_DIRTY=true` 组装成 `version[-commit][-dirty]`;缺少版本元数据时不显示徽标。New Session 优先使用作用域操作明确指定的 Workspace,否则使用当前 Session 所属 Workspace,再否则使用最近活跃 Workspace;一个 Workspace 都没有时则清空选择,进入空白 New Session 页面。 新建会话控件的 tooltip 和 `aria-keyshortcuts` 显示命令注册表中的当前有效绑定,侧栏隐藏时 macOS 常驻头部中的控件也保持一致。 +展开的品牌行把 `sidebar.brand.mark` 与 `sidebar.brand.name` 渲染为两个独立的 single slot;收起轨道则渲染同一个 mark slot。没有占位者时,外壳使用鱼形标记和本地化的本地构建标签。完整构建会在标签下方显示代码徽标;该徽标使用 `DSH_CLIENT_VERSION`、可选的 7 位 `DSH_CLIENT_COMMIT_HASH` 与 `DSH_CLIENT_GIT_DIRTY=true` 组装成 `version[-commit][-dirty]`;缺少版本元数据时不显示徽标。New Session 优先使用作用域操作明确指定的 Workspace,否则使用当前 Session 所属 Workspace,再否则使用最近活跃 Workspace;一个 Workspace 都没有时则清空选择,进入空白 New Session 页面。 展开态的新建会话按钮在悬停或键盘聚焦时于右侧显示当前有效绑定的灰色文字,图标与文字始终作为整体居中。右侧空间不足时,改用仅含快捷键的 tooltip。纯图标控件保留使用平台键位样式的 tooltip,侧栏隐藏时常驻的 macOS 头部控件也保持一致。所有控件均提供 `aria-keyshortcuts`。 ### 全局面板入口 diff --git a/packages/client/ui-sidebar/src/client/HeaderLeadingControls.tsx b/packages/client/ui-sidebar/src/client/HeaderLeadingControls.tsx index d1b7824e0c..8e65ad4ab0 100644 --- a/packages/client/ui-sidebar/src/client/HeaderLeadingControls.tsx +++ b/packages/client/ui-sidebar/src/client/HeaderLeadingControls.tsx @@ -26,10 +26,9 @@ export type HeaderLeadingControlsProps = export function HeaderLeadingControls({ toggleSidebar, startSession, useShortcuts, t }: HeaderLeadingControlsProps) { const shortcut = useShortcuts(rows => rows.find(row => row.id === 'sidebar.left.toggle')) const newShortcut = useShortcuts(rows => rows.find(row => row.id === 'session.new')) - const newHint = newShortcut?.keys.length ? t('shortcut.hint', { label: t('session.new.label'), keys: newShortcut.keys.join(' ') }) : t('session.new.label') return (
- + - + diff --git a/packages/client/ui-sidebar/tests/__snapshots__/sidebar-snapshot.client.spec.tsx.snap b/packages/client/ui-sidebar/tests/__snapshots__/sidebar-snapshot.client.spec.tsx.snap index a467c3dbba..79ec54b715 100644 --- a/packages/client/ui-sidebar/tests/__snapshots__/sidebar-snapshot.client.spec.tsx.snap +++ b/packages/client/ui-sidebar/tests/__snapshots__/sidebar-snapshot.client.spec.tsx.snap @@ -40,16 +40,20 @@ exports[`sidebar shell snapshots > renders Windows caption controls in expanded class="newSession" type="button" > - + + +
renders Windows caption controls in expanded class="newSession" type="button" > - - New Session + + + New Session +
renders the collapsed rail after the crossfad class="newSession" type="button" > - + + +
renders the expanded column (wordmark, capsul class="newSession" type="button" > - - New Session + + + New Session +
renders the expanded column in the default lo class="newSession" type="button" > - - 新会话 + + + 新会话 +
{ for (const button of screen.getAllByRole('button', { name: 'New session' })) { expect(button.getAttribute('aria-keyshortcuts')).toBe('Control+N') } + const expanded = screen.getAllByRole('button', { name: 'New session' }).find(button => button.querySelector('kbd') !== null)! + fireEvent.mouseEnter(expanded) + fireEvent.focus(expanded) + expect(screen.getByRole('tooltip').getAttribute('aria-label')).toBe('Ctrl N') + expect(Array.from(expanded.querySelectorAll('kbd'), key => key.textContent)).toEqual(['Ctrl', 'N']) cleanup() render( { const button = screen.getByRole('button', { name: 'New session' }) expect(button.getAttribute('aria-keyshortcuts')).toBe('Control+N') fireEvent.focus(button) - expect(screen.getByRole('tooltip').textContent).toContain('Ctrl N') + expect(Array.from(screen.getByRole('tooltip').querySelectorAll('kbd'), key => key.textContent)).toEqual(['Ctrl', 'N']) }) it('routes New Session (capsule + wordmark) and the column toggle', () => { const b = mountShell() @@ -306,5 +311,5 @@ it('uses the same effective sidebar binding for hover/focus hints and ARIA', () const toggle = screen.getByRole('button', { name: en['toggle.collapse'] }) expect(toggle.getAttribute('aria-keyshortcuts')).toBe('Meta+B') fireEvent.focus(toggle) - expect(screen.getByRole('tooltip').textContent).toContain('⌘ B') + expect(Array.from(screen.getByRole('tooltip').querySelectorAll('kbd'), key => key.textContent)).toEqual(['⌘', 'B']) }) diff --git a/packages/client/ui-theme/README.i18n.yaml b/packages/client/ui-theme/README.i18n.yaml index 70f49fbb64..f6602d231a 100644 --- a/packages/client/ui-theme/README.i18n.yaml +++ b/packages/client/ui-theme/README.i18n.yaml @@ -30,8 +30,8 @@ en: 8fbfdb050e880f9b zh: dd201f049036c5c4 /deepseek-ai-dsh-client-ui-theme/understand-the-implementation/stylesheets: - en: 1d398bcc78b09ef2 - zh: cc657eeb80b55a62 + en: 9e12626e970874b7 + zh: 7b4013d4305b4d2e /deepseek-ai-dsh-client-ui-theme/understand-the-implementation/scrollbar-rebinding: en: bf76e7c34a331595 zh: 5accd0260ef7c61e diff --git a/packages/client/ui-theme/README.md b/packages/client/ui-theme/README.md index 30cfb594cb..06bc74f848 100644 --- a/packages/client/ui-theme/README.md +++ b/packages/client/ui-theme/README.md @@ -59,7 +59,7 @@ The service owns theme and font-size state and publishes snapshots. The ui-layou `base.css` suppresses only the outline of focused elements marked `data-dsh-automatic-focus` by the [primitive focus helper](../ui-primitives/README.md); ordinary keyboard focus styling, borders, shadows, and error states remain intact. -System toasts use `--dsw-alias-toast-bg` and `--dsw-alias-toast-label` for a shared background and text color across callers. Document previews pair `--dsw-alias-bg-document-preview` with `--dsw-alias-label-document-preview` so the backdrop and status text follow the same theme. +System toasts use `--dsw-alias-toast-bg` and `--dsw-alias-toast-label` for a shared background and text color across callers. Document previews pair `--dsw-alias-bg-document-preview` with `--dsw-alias-label-document-preview` so the backdrop and status text follow the same theme. Tooltip keycaps use `--dsw-alias-tooltip-key-bg`, a lighter fill derived from the tooltip background in each palette. `brand-font.css` exports the local Montserrat Light, Regular and Medium faces (normal style, weights 300, 400 and 500), with `montserrat-light.woff2`, `montserrat-regular.woff2`, `montserrat-medium.woff2` and its SIL Open Font License in `lib/styles/`. Desktop bundles the same stylesheet, font and license for offline welcome brand text; ordinary UI keeps its system font stack. diff --git a/packages/client/ui-theme/README.zh.md b/packages/client/ui-theme/README.zh.md index 3ccc8901a6..d7f1debd71 100644 --- a/packages/client/ui-theme/README.zh.md +++ b/packages/client/ui-theme/README.zh.md @@ -59,7 +59,7 @@ kind: "package-reference" `base.css` 仅抑制[基础控件焦点工具](../ui-primitives/README.zh.md)通过 `data-dsh-automatic-focus` 标记的聚焦元素外轮廓线;正常键盘焦点样式、边框、阴影及错误状态保持不变。 -系统提示使用 `--dsw-alias-toast-bg` 和 `--dsw-alias-toast-label`,在各调用方之间统一背景与文字颜色。文档预览配对使用 `--dsw-alias-bg-document-preview` 与 `--dsw-alias-label-document-preview`,使底色与状态文字遵循相同主题。 +系统提示使用 `--dsw-alias-toast-bg` 和 `--dsw-alias-toast-label`,在各调用方之间统一背景与文字颜色。文档预览配对使用 `--dsw-alias-bg-document-preview` 与 `--dsw-alias-label-document-preview`,使底色与状态文字遵循相同主题。Tooltip 键帽使用 `--dsw-alias-tooltip-key-bg`,由各主题的 tooltip 背景派生稍浅的填充。 `brand-font.css` 导出本地 Montserrat Light、Regular 和 Medium 字体(正体、字重 300、400 和 500),`lib/styles/` 同时提供 `montserrat-light.woff2`、`montserrat-regular.woff2`、`montserrat-medium.woff2` 及其 SIL Open Font License。Desktop 将同一份样式表、字体和许可证打包,用于欢迎页品牌文字的离线显示;普通界面保留系统字体栈。 diff --git a/packages/client/ui-theme/src/styles/design-platform.css b/packages/client/ui-theme/src/styles/design-platform.css index 7b9ee141d6..672e90c20a 100644 --- a/packages/client/ui-theme/src/styles/design-platform.css +++ b/packages/client/ui-theme/src/styles/design-platform.css @@ -256,6 +256,7 @@ body { --dsw-alias-toast-bg: var(--dsw-static-neutral-bluish-800); --dsw-alias-toast-label: var(--dsw-static-neutral-bluish-00); --dsw-alias-tooltip-bg: var(--dsw-static-neutral-bluish-850); + --dsw-alias-tooltip-key-bg: color-mix(in srgb, var(--dsw-alias-tooltip-bg), white 18%); --dsw-specific-bubble-highlight: var(--dsw-static-deepseek-200); --dsw-specific-bubble: var(--dsw-static-deepseek-50); --dsw-specific-input-major: var(--dsw-static-neutral-bluish-00); @@ -365,6 +366,7 @@ body[data-ds-dark-theme] { --dsw-alias-toast-bg: var(--dsw-static-neutral-bluish-750); --dsw-alias-toast-label: var(--dsw-static-neutral-bluish-00); --dsw-alias-tooltip-bg: var(--dsw-static-neutral-bluish-750); + --dsw-alias-tooltip-key-bg: color-mix(in srgb, var(--dsw-alias-tooltip-bg), white 18%); --dsw-specific-bubble-highlight: var(--dsw-static-neutral-bluish-750); --dsw-specific-bubble: var(--dsw-static-neutral-bluish-850); --dsw-specific-input-major: var(--dsw-static-neutral-bluish-850); diff --git a/packages/client/ui-theme/tests/expected/radius-exceptions.expected.json b/packages/client/ui-theme/tests/expected/radius-exceptions.expected.json index f4a7729d1a..fad12ea46f 100644 --- a/packages/client/ui-theme/tests/expected/radius-exceptions.expected.json +++ b/packages/client/ui-theme/tests/expected/radius-exceptions.expected.json @@ -1,7 +1,4 @@ { - "ui-primitives/src/ShortcutKeys.module.css": [ - ".keys: border-radius: 10px" - ], "ui-schedule/src/client/ClockPicker.module.css": [ ".option: border-radius: 10px" ], @@ -58,7 +55,7 @@ ".dialog: border-radius: 24px", ".close: border-radius: 28px", ".searchField: border-radius: 12px", - ".keyButton: border-radius: 10px", + ".keyBadge: border-radius: 10px", ".unbound: border-radius: 10px", ".recorder: border-radius: 10px" ] diff --git a/packages/client/ui-workspace/src/client/rows/Rows.tsx b/packages/client/ui-workspace/src/client/rows/Rows.tsx index bdf201e7e9..635770fd47 100644 --- a/packages/client/ui-workspace/src/client/rows/Rows.tsx +++ b/packages/client/ui-workspace/src/client/rows/Rows.tsx @@ -294,7 +294,7 @@ export function ProjectRowItem({ group, containsCurrentDescendant = false, onTog )} /> )} - +