fix(workspace): align unnamed rename entry points and hover actions

This commit is contained in:
liyao committed 2026-09-24 17:31:42 +08:00
1 parent 274bb97e0a
commit a4ac7e9eb3
19 files changed
+53 -30

No files matched your search

+4 -4
View File
@@ -6,8 +6,8 @@
en: 45561fc0dd3a1b20
zh: f4b03355d4bd0693
/deepseek-harness-desktop/closing-the-window-and-quitting:
en: fb852f3bd70d8602
zh: 3d2b0a8d580d7f0a
en: c323e02e3f0305fe
zh: 3c730db632136443
/deepseek-harness-desktop/key-technical-decisions:
en: 069fdfd72d2ea26a
zh: 76ae549d43c99bb4
@@ -66,5 +66,5 @@
en: 5b77213c86bbcfb6
zh: b34bf21773bb852b
/deepseek-harness-desktop/dev-note:
en: 2b5d500798edeb32
zh: 901a0e32ba2ff8be
en: 8e26b460ba31f21b
zh: fbcd0307b3867125
+2 -2
View File
@@ -28,6 +28,8 @@ Windows keeps a tray icon for the whole run. Its tooltip is the product name, a
Every ordinary quit entry — ⌘Q, the application menu, the Dock menu, the Windows tray and caption Application menu, and the quits caused by closing the mandatory-update or welcome window — asks the Host what it would interrupt. The Host answers over the private IPC channel with two facts: active tasks under the same rule as the update restart check (running agents including subagents and approval waits, queued messages, running or stopping jobs), and armed scheduled reminders reported by the `schedule` family of `workspace/session-activity` for the sessions loaded during this run. Neither fact, the quit proceeds silently. Otherwise a native message box without an owner window — hidden windows stay hidden — shows **Quit DeepSeek Harness?** with one of three localized explanations: running tasks will be interrupted, scheduled tasks will not run while the application is closed, or both. Quit is the default button and Cancel answers Esc; macOS places Cancel left of Quit, Windows places Quit left of Cancel, and the Windows task dialog shows the application icon and stays light regardless of the application theme. A Host that has not reached ready or has failed cannot run tasks, so the quit proceeds without asking. An inspection failure or a Host that misses the two-second deadline is treated as running tasks. While the box is open, another quit request joins it instead of stacking a second one (macOS also raises it; Electron exposes no handle to the Windows task dialog), the explanation does not change when work starts or ends, Quit stops the application without re-inspecting, and Cancel changes nothing. Cancelling a quit that closing the welcome window started shows the welcome window again.
The mandatory update confirmation includes the installation wait notice on Windows; macOS uses the shorter restart explanation.
The confirmation is skipped when the installer restart already confirmed task interruption, when the fatal-recovery dialog exits or restarts, when the development Restart App and Host command runs, and during operating-system shutdown, restart, or log-off: Windows sets this on the definitive session-end message; macOS sets it on the power-off notification, and because another application can still cancel that shutdown, the next focus or show of the main window clears it. Installer-owned quit cancels any pending ordinary quit decision; late inspection results and dialog answers cannot open another confirmation or repeat cleanup. A user-initiated update download that finishes while the window is hidden defers its Install and Restart confirmation until the window is shown again; the mandatory flow keeps its taskbar and Dock attention. The Windows installer and uninstaller tell a user whose application is still running to quit it from the system tray. Desktop does not enable scheduled tasks by default, so the scheduled-task explanations appear only once that feature is on; reminders fire only in loaded sessions, and unloaded sessions are neither counted nor resumed until they are opened.
The tray renderer enlarges the whale by 20% around the tile center while preserving the background and aspect ratio; application and installer icons retain their original proportions.
@@ -457,5 +459,3 @@ The account provider’s `embeddedPageDist` configuration adds a `dist` query pa
## Dev Note
Pre-launch CDN and capacity decisions are tracked in the [Desktop update proposal](../../.agents/notes/proposed/feature/2026-09-08-desktop-update-policy-and-installation.md#cdn-and-capacity-qualification).
The mandatory update confirmation includes the installation wait notice on Windows; macOS uses the shorter restart explanation.
+2 -2
View File
@@ -28,6 +28,8 @@ Windows 在整个运行期间常驻托盘图标。悬停提示为产品名,单
所有普通退出入口——⌘Q、应用菜单、Dock 菜单、Windows 托盘和标题栏"应用程序"菜单,以及关闭强制更新窗口或欢迎窗口引起的退出——都先向 Host 查询退出会中断什么。Host 通过私有 IPC 通道回答两项事实:与更新重启检查同一口径的运行中任务(运行中的 agent,包括子代理和等待审批的回合、排队消息、运行中或停止中的后台任务),以及本次运行中已加载会话里由 `workspace/session-activity` 的 `schedule` family 报告的已挂定时器的提醒。两项都没有时直接退出,不弹框。否则弹出一个没有父窗口的原生消息框——隐藏的窗口保持隐藏——标题为**退出 DeepSeek Harness?**,正文为三种本地化说明之一:正在运行的任务将会中断、应用关闭期间定时任务不会运行,或两者兼有。"退出"是默认按钮,Esc 等同"取消";macOS 上"取消"在"退出"左侧,Windows 上"退出"在"取消"左侧,Windows 任务对话框显示应用图标且不跟随应用主题、始终为浅色。Host 尚未就绪或已失败时不可能有任务在跑,直接退出。查询失败或 Host 超过两秒截止时间未答复,按运行中任务处理。弹框打开期间,再次请求退出只会并入同一弹框而不叠加新弹框(macOS 上还会把它提到前面;Electron 不暴露 Windows 任务对话框的句柄);任务开始或结束不会改变文案;点"退出"不再重新查询即停止应用;点"取消"不发生任何变化。取消由关闭欢迎窗口引起的退出时,欢迎窗口会重新显示。
强制更新的安装确认在 Windows 上额外显示等待提示,macOS 只显示简短的重启说明。
以下情况跳过确认:安装更新的重启已确认过任务中断、致命错误恢复对话框中的退出或重启、开发版"重启应用与 Host"命令,以及操作系统关机、重启或注销:Windows 在确定性的会话结束消息上设置该状态;macOS 在关机通知上设置,而其他应用仍可能取消这次关机,因此主窗口下一次获得焦点或显示时会清除它。安装器接管退出时会取消尚未结束的普通退出决策;晚到的查询结果和弹框答复不会再次打开确认框或重复清理。窗口隐藏期间完成的用户主动发起的更新下载,把"安装并重启"确认推迟到窗口再次显示时;强制更新流程沿用其任务栏和 Dock 提醒。Windows 安装程序和卸载程序在应用仍在运行时提示用户先在系统托盘中退出。Desktop 默认未开启定时任务,定时任务的说明只在该功能开启后出现;提醒只在已加载的会话中触发,未加载的会话既不计入,也要等到打开后才会继续。
托盘渲染器以底板中心为基准将鲸鱼放大 20%,保留背景和宽高比;应用和安装器图标保持原有比例。
@@ -459,5 +461,3 @@ node apps/desktop/node_modules/pnpm/bin/pnpm.mjs --dir apps/desktop run test:upd
## 开发备注
上线前 CDN 与容量决策见[桌面更新提案](../../.agents/notes/proposed/feature/2026-09-08-desktop-update-policy-and-installation.zh.md#cdn-and-capacity-qualification)。
强制更新的安装确认在 Windows 上额外显示等待提示,macOS 只显示简短的重启说明。
+1 -1
View File
@@ -793,7 +793,7 @@ async function main(): Promise<void> {
if (manual) {
const result = await ordinaryMessageBox({ title: locale.messages.updateCheckTitle,
message: formatDesktopMessage(locale.messages.updateAvailable, { version: state.version ?? '' }),
detail: formatDesktopMessage(locale.messages.updateDetail, { version: state.version ?? '' }),
detail: locale.messages.updateDetail,
buttons: [locale.messages.updateDownload], cancelId: 1 })
if (result.response !== 0) return
}
+1 -1
View File
@@ -218,7 +218,7 @@ async function qualify() {
const checking = await dialogWith(messages.updateChecking)
await screenshot(checking, 'checking.png')
server.release()
const current = await dialogWith(messages.updateCurrent.replace('{version}', app.getVersion()))
const current = await dialogWith(messages.updateCurrent)
assert.equal(current, checking)
await clickText(current, messages.updateAcknowledge)
assert.equal(server.requests.filter(path => path === '/payload.exe').length, 0)
@@ -205,3 +205,10 @@ it('rebinds policy updates and reloads to a replacement Windows main window', ()
sender: window.webContents, senderFrame: window.webContents.mainFrame,
})).not.toThrow()
})
it.each(['darwin', 'win32'] as const)('includes the installation wait notice only on Windows: %s', (platform) => {
const f = setup(platform)
expect(f.view().locale.messages.mandatoryReadyDetail).toBe(platform === 'win32'
? '更新期间应用将暂时关闭,完成后会自动打开。\n\n更新可能需要一些时间,请耐心等待,期间请勿重复启动应用。'
: '更新期间应用将暂时关闭,完成后会自动打开。')
})
@@ -16,7 +16,7 @@
zh: 45a29bea90cff239
/deepseek-ai-dsh-client-ui-settings-general/use-this-package:
en: 473e1f0843f7a896
zh: eaae0c2618b7f93c
zh: acfbc21efc458bbf
/deepseek-ai-dsh-client-ui-settings-general/use-this-package/the-general-section:
en: de484a8406f3fc17
zh: a3831774d457c49a
@@ -31,7 +31,7 @@ kind: "package-reference"
当分区导航超出面板可用高度时,列表独立于设置内容滚动,并保持「设置」标题固定。
在 Desktop 中,账户行更新控件显示可用版本、进度、验证、就绪和持久重试反馈。它与连接指示器共用 28px 高度、8px 圆角、14px 图标占位、4px 图文间距,以及 12px 中等字重文字和 18px 行高;更新状态的边框、填充和文字颜色独立定义。重试文字及其圆点与其他更新文案使用相同的品牌蓝色;数字下载进度不带省略号。预加载仅传递语义化阶段、版本、进度和失败类别;组件从当前 `settings` locale 解析全部可见与无障碍文案,并在应用内语言变化后同步更新。选择可用更新即开始下载;安装需要独立的壳拥有的确认。侧栏收起时,其顶部展开按钮以品牌蓝色圆点显示相同状态,包括失败状态。连接反馈通常优先展示,但壳报告正在安装时,预期的后端断开不能遮盖更新状态;失败后恢复连接反馈。两个控件共用一次载体订阅;浏览器代码不能选择安装包或授权安装。[Desktop 更新](../../../apps/desktop/README.zh.md)负责发布流程。 检查中、校验中和准备重启状态的悬浮提示只显示已提供的版本号;没有版本号时保留状态文案。桌面更新提示完整保留版本号及预发布后缀,不额外添加 v 或 V;错误提示与桌面升级弹窗保持一致。
在 Desktop 中,账户行更新控件显示可用版本、进度、验证、就绪和持久重试反馈。它与连接指示器共用 28px 高度、8px 圆角、14px 图标占位、4px 图文间距,以及 12px 中等字重文字和 18px 行高;更新状态的边框、填充和文字颜色独立定义。重试文字及其圆点与其他更新文案使用相同的品牌蓝色;数字下载进度不带省略号。预加载仅传递语义化阶段、版本、进度和失败类别;组件从当前 `settings` locale 解析全部可见与无障碍文案,并在应用内语言变化后同步更新。选择可用更新即开始下载;安装需要独立的壳拥有的确认。侧栏收起时,其顶部展开按钮以品牌蓝色圆点显示相同状态,包括失败状态。连接反馈通常优先展示,但壳报告正在安装时,预期的后端断开不能遮盖更新状态;失败后恢复连接反馈。两个控件共用一次载体订阅;浏览器代码不能选择安装包或授权安装。[Desktop 更新](../../../apps/desktop/README.zh.md)负责发布流程。检查中、校验中和准备重启状态的悬浮提示只显示已提供的版本号;没有版本号时保留状态文案。桌面更新提示完整保留版本号及预发布后缀,不额外添加 v 或 V;错误提示与桌面升级弹窗保持一致。
设置的可见状态和分区选择由 shell owner store 保存。外壳将设置命令的有效绑定传给入口贡献,用于菜单键帽和 `aria-keyshortcuts`;回退按钮使用相同绑定提供悬停、键盘聚焦提示和 `aria-keyshortcuts`。设置位于最前方或没有模态弹窗时,设置命令(默认为 `Mod+,`)切换设置弹窗的开关;侧栏控件打开同一弹窗。快捷键速查或其他模态弹窗位于前方时,该命令不能打开或关闭设置。长按产生的重复按键不执行操作。初始焦点落在导航中选中的分区;没有可用分区时落在标题上,均不绘制焦点外轮廓线。Tab 和方向键导航保留可见焦点提示。`Mod+/` 可在设置上方打开快捷键速查;`Escape` 关闭最上层弹窗,焦点返回触发该弹窗的控件。通过快捷键、Esc 或鼠标关闭后的回焦均不绘制外轮廓线。
@@ -15,8 +15,8 @@
en: d152484eb41ac6b4
zh: 09388d293f9be9cb
/deepseek-ai-dsh-client-ui-workspace/use-this-package:
en: c8d579e1e65bc8c5
zh: 56f8e45351445ec7
en: b164014ee565be16
zh: 6a741647932b93e6
/deepseek-ai-dsh-client-ui-workspace/use-this-package/reordering-and-view-options:
en: 1fe0693758e0536b
zh: ef30198bc767fc04
@@ -69,5 +69,5 @@
en: 00d7b1ddda7390f5
zh: 1e5a1055d1be4d1d
/deepseek-ai-dsh-client-ui-workspace/known-limitations-and-deferred-work/dev-note:
en: 72e98246e3f8430c
zh: 5d17178d6122cc42
en: 585813ff2c68c20c
zh: 18b43c0d64fa8a72
+2 -2
View File
@@ -27,6 +27,8 @@ This package lets users browse grouped or flat Session lists, choose a Workspace
Use the sidebar to browse Workspaces and their Sessions, reorder them, and start new ones; use the picker in the Session Intent hero to choose a Workspace for a new session. An open Workspace shows five idle, non-blank Sessions by default. Running Sessions, including parents with running children, remain visible in their ordered positions without using that quota; the selected blank **New Session** is also an extra row until its first prompt. Each **Show more** click reveals up to five more idle Sessions; after the final batch, **Show less** restores the initial rows while keeping running Sessions visible. Closing and reopening the Workspace also restores this folded projection.
History rows without a stored title use the localized unnamed label (未命名 / Untitled), rather than a directory name. The current blank row remains New Session; other blank rows remain hidden. Rename drafts use the stored title, or start empty when unnamed; unnamed rows do not offer title copying.
### Reordering and view options
Pinned Sessions lead ordinary Sessions in both grouped and flat views. **Last updated** sorts each partition strictly by the latest user prompt or steer time, newest first; pin time does not affect it. **Manual** uses the relative positions in one complete Session sequence, including hidden archives. Returning to Last updated discards the manual layout, and entering Manual again freezes the then-current chronological order. The browser defaults to Last updated and remembers the selected mode across reloads.
@@ -222,5 +224,3 @@ None.
</details>
**Runtime invariant:** No companion is published. This is a pure-consumer plugin that registers presentational components into two host-declared slots and registers its locale dictionaries; its inject face consists of stateless RPC wrappers plus a create-and-open call. It emits no Cordis events and owns no cross-plugin mutable state.
History rows without a stored title use the localized unnamed label (未命名 / Untitled), rather than a directory name. The current blank row remains New Session; other blank rows remain hidden.
+2 -2
View File
@@ -27,6 +27,8 @@ kind: "package-reference"
用侧边栏浏览 Workspace 及其 Session、重排它们并新建会话;在 Session Intent 主视觉区用选择器为新会话选择 Workspace。打开的 Workspace 默认显示五条空闲的非空白 Session。正在运行的 Session(包括有子会话正在运行的父会话)始终按原顺序显示,不占用这五条配额;当前选中的空白**新会话**在首条提示词落地前也作为额外行。每次点击**展开其余**最多再显示五条空闲 Session;全部显示后,**收起**恢复初始行数,但仍显示正在运行的 Session。关闭再打开 Workspace 也会恢复该折叠投影。
未保存标题的历史会话显示本地化名称「未命名 / Untitled」,不使用目录名兜底。当前空会话仍显示「新会话」,其他空会话仍隐藏。重命名草稿使用已保存标题,无标题时留空;无标题行不提供标题复制。
### 重排序与视图选项
分组和平铺视图都先显示置顶 Session,再显示普通 Session。**最近更新**在各分区内严格按最近一次用户提示词或 steering(中途引导)时间降序排列,置顶时间不影响它。**手动排序**使用同一条完整 Session 序列中的相对位置,包括隐藏的归档项。返回最近更新会丢弃手动布局,再次进入手动排序时冻结当时的时间顺序。浏览器默认采用最近更新,并在重新加载后记住所选模式。
@@ -222,5 +224,3 @@ Workspace 与 Session 悬浮卡片会复制对应行被截断的值:激活 Wor
</details>
**运行时不变式:** 不发布伴生入口。这是一个纯消费方插件,只向两个由宿主声明的 slot 注册展示组件,并注册自身的 locale dictionaries;inject face 由无状态 RPC 包装层和一次 create-and-open 调用组成;本插件不发出 Cordis 事件,也不持有跨插件可变状态。
未保存标题的历史会话显示本地化名称「未命名 / Untitled」,不使用目录名兜底。当前空会话仍显示「新会话」,其他空会话仍隐藏。
@@ -76,7 +76,7 @@ export interface DirectoryFlowOwnerProps {
export interface SessionRowOwnerProps {
/** Session the row shows. */
sessionId: SessionId
/** Row display title: persisted title or the localized unnamed label. */
/** Row display title: persisted title, or empty when the Session has none. */
displayTitle: string
}
@@ -695,7 +695,7 @@ export function SessionNodeItem({
content={<SessionHoverContent node={node} now={now} renderSlot={renderSlot} t={t} />}
openDelayMs={800}
disabled={menuOpen || drag?.active === true}
copyText={row.blank ? undefined : row.title}
copyText={row.blank || row.title === '' ? undefined : row.title}
copyLabel={t('copy')}
copiedLabel={t('hover.copied')}
/>
@@ -40,7 +40,7 @@ export function RenameSessionMenuItem({
* one dialog per request (keyed by the Session, so a new request starts a
* fresh draft). Sessions have no client-side name-conflict rule (the host
* normalizes), and unlike Workspace rename an unchanged title is NOT
* blocked: confirming the current automatic title is the gesture that pins it.
* blocked. An unnamed Session starts with an empty draft and requires a name.
* @param props - the request hook, its settlement, the rename hop, and the locale seat.
* @returns the open dialog, or null.
*/
@@ -96,7 +96,7 @@ export function installWorkspaceShortcuts(
register('session.rename', () => t('rename.session.title'), ['rename session'], 'KeyR', ['primary', 'alt'], ['primary', 'shift'], () => {
const target = current()
return target === undefined ? { status: 'blocked', reason: t('shortcut.noSession') }
: { status: 'handled', run: () => { controls.rename(target.id, target.displayTitle) } }
: { status: 'handled', run: () => { controls.rename(target.id, target.title?.trim() ?? '') } }
})
register('session.fork', () => t('menu.fork'), ['fork session'], 'KeyF', ['primary', 'alt'], ['primary', 'shift'], () => {
const target = current()
@@ -43,7 +43,7 @@ function mainSessionId(list: SessionListState): SessionId | undefined {
/** One top-level session row in a group or the flat list. */
export interface SessionNode {
id: SessionId
/** Stored display title; the renderer substitutes the localized New Session label for blank rows. */
/** Stored title, or empty; the renderer localizes blank and unnamed row labels. */
title: string
/** The provisional blank session (renderer shows the localized New Session title). */
blank: boolean
@@ -289,10 +289,11 @@ function sectionMembers(
/**
* A blank session is the selected Workspace's provisional New Session row;
* its canonical title never enters search (blank rows are query-excluded)
* and the renderer localizes its display label.
* and the renderer localizes its display label. Unnamed history also yields an
* empty title for localization and does not match a directory-name title search.
*/
function sessionTitle(session: SessionSummary): string {
return session.blank ? '' : (session.title?.trim() || '')
return session.blank ? '' : (session.title?.trim() ?? '')
}
/** Build one group without projecting session lineage into presentation. */
@@ -1084,9 +1084,12 @@ describe('session row schedule seats', () => {
it.each([['未命名', t], ['Untitled', tEn]])('labels unnamed history as %s', (label, translate) => {
const node: SessionNode = { id: sid('unnamed'), title: '', blank: false, running: false,
runningSubagentCount: 0, completed: false, updatedAt: 0, pinned: false, archived: false }
const onRenameRequest = vi.fn()
render(<SessionNodeItem node={node} currentId={node.id} now={0} onOpen={vi.fn()}
renderSlot={() => null} t={translate} />)
renderSlot={() => null} onRenameRequest={onRenameRequest} t={translate} />)
expect(screen.getByText(label)).toBeTruthy()
fireEvent.doubleClick(screen.getByText(label))
expect(onRenameRequest).toHaveBeenCalledWith(node.id, '')
})
it.each([['未命名', t], ['Untitled', tEn]])('labels unnamed search results as %s', (label, translate) => {
@@ -168,3 +168,15 @@ describe('workspace shortcut ownership', () => {
expect(b.loadOlder).not.toHaveBeenCalled()
})
})
it('opens an empty rename draft for unnamed history instead of its directory label', async () => {
const b = await bench()
const unnamed = row('a', true)
delete unnamed.title
unnamed.displayTitle = 'Default workspace'
b.list.set({ ...b.list.getSnapshot(), byId: { [unnamed.id]: unnamed } })
const command = b.commands.get('session.rename')!.resolve(context)
expect(command.status).toBe('handled')
if (command.status === 'handled') command.run()
expect(b.controls.state.getSnapshot().renameTarget).toEqual({ sessionId: 'a', currentTitle: '' })
})
@@ -3849,7 +3849,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
},
],
ownerProps: [
'/** Owner share of one Session row action occurrence: the row the action belongs to. */\nexport interface SessionRowOwnerProps {\n /** Session the row shows. */\n sessionId: SessionId\n /** Row display title: persisted title or the localized unnamed label. */\n displayTitle: string\n}',
'/** Owner share of one Session row action occurrence: the row the action belongs to. */\nexport interface SessionRowOwnerProps {\n /** Session the row shows. */\n sessionId: SessionId\n /** Row display title: persisted title, or empty when the Session has none. */\n displayTitle: string\n}',
],
ownerPropsReferences: [
'SessionId',
@@ -3904,7 +3904,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
},
],
ownerProps: [
'/** Owner share of one Session row action occurrence: the row the action belongs to. */\nexport interface SessionRowOwnerProps {\n /** Session the row shows. */\n sessionId: SessionId\n /** Row display title: persisted title or the localized unnamed label. */\n displayTitle: string\n}',
'/** Owner share of one Session row action occurrence: the row the action belongs to. */\nexport interface SessionRowOwnerProps {\n /** Session the row shows. */\n sessionId: SessionId\n /** Row display title: persisted title, or empty when the Session has none. */\n displayTitle: string\n}',
],
ownerPropsReferences: [
'SessionId',