fix(composer): clarify DSH actions and keep stop confirmation inline
Name the browser and service controls explicitly for DSH. Keep confirmation and cancellation together in the composer action row, retain the interruption warning as a tooltip and accessible description, and show stopping progress on the confirmation button. Validation: 20 focused tests passed; typecheck and lint passed. Release note: 明确 DSH 网页和服务按钮名称,停止确认与取消按钮同行显示,避免确认区单独撑开聊天输入区。
This commit is contained in:
@@ -18,7 +18,7 @@ Status: Local shortcut/coordinator paths and configured-model responses verified
|
||||
| --- | --- | --- | --- |
|
||||
| Continue | 在终端中打开 | 工作栏新终端页签,启动原生 CLI | 本地和托管 SSH 项目 |
|
||||
| OpenCode | 在终端中打开 | 工作栏新终端页签,启动原生 TUI | 本地和托管 SSH 项目 |
|
||||
| DeepSeek Harness | 在浏览器中打开 | 系统默认浏览器,访问官方 Web 应用 | 本地项目 |
|
||||
| DeepSeek Harness | 打开DSH Web | 系统默认浏览器,访问官方 Web 应用 | 本地项目 |
|
||||
| 内置模型 | 不显示此快捷入口 | 无 | 保持现状 |
|
||||
|
||||
DS 采用官方 `@deepseek-ai/dsh` Web 应用,不开发新的 DS 终端前端,也不在首轮嵌入
|
||||
@@ -164,7 +164,7 @@ DS 后台的 `read` 和原生 `skill` 可在 Ask 执行;`write`、`edit`、`pw
|
||||
|
||||
## 5. 界面设计
|
||||
|
||||
输入区下方从左到右排列“在终端中打开/在浏览器中打开”“压缩上下文”和现有用量信息。
|
||||
输入区下方从左到右排列“在终端中打开/打开DSH Web”“压缩上下文”和现有用量信息。
|
||||
两个按钮独立判断可用性。采用同一轻量操作组,避免两组绝对定位相互覆盖;窄输入区允许
|
||||
操作组换行,保留模式说明、用量信息和对称边距。稳定预留元信息高度的实现应与
|
||||
[统一 UI 规范](../../../UI-DESIGN.md)一起更新并验证。
|
||||
@@ -173,8 +173,9 @@ DS 后台的 `read` 和原生 `skill` 可在 Ask 执行;`write`、`edit`、`pw
|
||||
进入新终端;外部浏览器由系统处理焦点。启动期间保留按钮宽度并显示“正在打开…”,完成后
|
||||
恢复原文案。缺少项目目录时在相关位置提供选择目录的操作;其他异步失败进入全局通知视口。
|
||||
|
||||
DS 服务运行后,在入口旁提供轻量菜单“重新打开”“停止此服务”;操作指向当前匹配服务。
|
||||
若正在生成,停止前说明会中断该服务中的请求。输入区不常驻显示独立会话和服务生命周期说明。不在每次启动时增加确认弹窗。
|
||||
DS 服务运行后,打开入口仍显示“打开DSH Web”,旁边显示“停止DSH服务”;操作指向当前匹配服务。
|
||||
点击停止后,在同一操作行追加紧凑的“确认”“取消”按钮,不再展开整行警告。窄窗口下确认和取消作为一组换行,两个按钮不拆行。
|
||||
确认按钮的悬停提示和无障碍描述说明网页会断开、进行中的请求会中断;执行时由确认按钮显示“正在停止…”。输入区不常驻显示独立会话和服务生命周期说明。不在每次启动时增加确认弹窗。
|
||||
|
||||
按钮快捷入口根据所选 Runtime 显示,不改变工作栏“+”中的普通终端和浏览器入口。已有服务
|
||||
的停止入口应在切换回来后仍可找到。
|
||||
|
||||
@@ -82,7 +82,7 @@ Agent 配置、Shell 执行和系统消息接口。
|
||||
和 Electron UI 测试失败;未取得全量通过结果,未修改这些并行工作的文件。
|
||||
|
||||
本次依赖升级回归的真实供应商模型调用为 0 次;后续真实模型及 Web 实测见上节。上述升级验证不包含官方 Web 应用,
|
||||
“在浏览器中打开”入口仍按[原生客户端方案](../assistant-workbar/runtime-native-client-proposal.md)实施。
|
||||
“打开DSH Web”入口仍按[原生客户端方案](../assistant-workbar/runtime-native-client-proposal.md)实施。
|
||||
远程启动契约和资源打包仅支持 Continue、OpenCode,`create-runtime.ts` 拒绝 SSH
|
||||
执行空间使用 DS,因此本次升级未改变远程 Runtime 路径,也未进行 Linux Host 验证。
|
||||
|
||||
|
||||
@@ -10677,7 +10677,7 @@ describe("App", () => {
|
||||
await screen.findByRole("textbox", { name: "向 GoodBuddy 提问" });
|
||||
await waitFor(() => expect(api.settings.getRuntime).toHaveBeenCalled());
|
||||
expect(screen.queryByRole("button", { name: "在终端中打开" })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "在浏览器中打开" })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "打开DSH Web" })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("opens local DS in the external browser without opening the workbar", async () => {
|
||||
@@ -10688,13 +10688,13 @@ describe("App", () => {
|
||||
}]);
|
||||
vi.mocked(api.openRuntimeNativeClient).mockResolvedValue({ kind: "browser", serviceId: "ds-service" });
|
||||
render(<App />);
|
||||
const open = await screen.findByRole("button", { name: "在浏览器中打开" });
|
||||
const open = await screen.findByRole("button", { name: "打开DSH Web" });
|
||||
await waitFor(() => expect(api.getRuntimeNativeClient).toHaveBeenCalledWith({ conversationId }));
|
||||
fireEvent.click(open);
|
||||
await waitFor(() => {
|
||||
expect(api.openRuntimeNativeClient).toHaveBeenCalledWith({ conversationId });
|
||||
expect(screen.getByRole("button", { name: "重新打开" })).toBeEnabled();
|
||||
expect(screen.getByRole("button", { name: "停止此服务" })).toBeEnabled();
|
||||
expect(screen.getByRole("button", { name: "打开DSH Web" })).toBeEnabled();
|
||||
expect(screen.getByRole("button", { name: "停止DSH服务" })).toBeEnabled();
|
||||
});
|
||||
expect(screen.getByRole("button", { name: "切换助手工作栏" })).toHaveAttribute("aria-expanded", "false");
|
||||
expect(api.terminal.create).not.toHaveBeenCalled();
|
||||
|
||||
@@ -49,17 +49,17 @@ describe('RuntimeNativeClientActions', () => {
|
||||
it('refreshes the matching DS service on selection changes and stops only that service', async () => {
|
||||
api.getRuntimeNativeClient.mockResolvedValueOnce({ serviceId: 'first-service' }).mockResolvedValue({ serviceId: 'second-service' })
|
||||
const { rerender } = render(<RuntimeNativeClientActions {...props} browser />)
|
||||
await screen.findByRole('button', { name: 'Reopen' })
|
||||
await screen.findByRole('button', { name: 'Stop DSH service' })
|
||||
rerender(<RuntimeNativeClientActions {...props} browser contextKey="second" />)
|
||||
expect(screen.queryByRole('button', { name: 'Stop this service' })).not.toBeInTheDocument()
|
||||
await screen.findByRole('button', { name: 'Reopen' })
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Stop this service' }))
|
||||
expect(screen.queryByRole('button', { name: 'Stop DSH service' })).not.toBeInTheDocument()
|
||||
await screen.findByRole('button', { name: 'Stop DSH service' })
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Stop DSH service' }))
|
||||
expect(api.stopRuntimeNativeClient).not.toHaveBeenCalled()
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Stop service' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Confirm' }))
|
||||
await waitFor(() => {
|
||||
expect(api.stopRuntimeNativeClient).toHaveBeenCalledWith({ serviceId: 'second-service' })
|
||||
expect(screen.getByRole('button', { name: 'Open in browser' })).toBeEnabled()
|
||||
expect(screen.queryByRole('button', { name: 'Stop this service' })).not.toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: 'Open DSH Web' })).toBeEnabled()
|
||||
expect(screen.queryByRole('button', { name: 'Stop DSH service' })).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -68,13 +68,14 @@ describe('RuntimeNativeClientActions', () => {
|
||||
render(<RuntimeNativeClientActions {...props} browser />)
|
||||
await waitFor(() => expect(api.getRuntimeNativeClient).toHaveBeenCalledTimes(1))
|
||||
api.getRuntimeNativeClient.mockResolvedValue({ serviceId: 'ready-service' })
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Open in browser' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Open DSH Web' }))
|
||||
await waitFor(() => {
|
||||
expect(props.notify).toHaveBeenCalledWith({ tone: 'error', message: 'Browser could not open' })
|
||||
expect(screen.getByRole('button', { name: 'Reopen' })).toBeEnabled()
|
||||
expect(screen.getByRole('button', { name: 'Open DSH Web' })).toBeEnabled()
|
||||
expect(screen.getByRole('button', { name: 'Stop DSH service' })).toBeEnabled()
|
||||
})
|
||||
api.openRuntimeNativeClient.mockResolvedValue({ kind: 'browser', serviceId: 'ready-service' })
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Reopen' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Open DSH Web' }))
|
||||
await waitFor(() => expect(api.openRuntimeNativeClient).toHaveBeenCalledTimes(2))
|
||||
expect(props.onTerminal).not.toHaveBeenCalled()
|
||||
})
|
||||
@@ -87,18 +88,39 @@ describe('RuntimeNativeClientActions', () => {
|
||||
rerender(<RuntimeNativeClientActions {...props} browser contextKey="second" />)
|
||||
await waitFor(() => expect(api.getRuntimeNativeClient).toHaveBeenCalledTimes(2))
|
||||
await act(async () => finish({ serviceId: 'old-service' }))
|
||||
expect(screen.queryByRole('button', { name: 'Stop this service' })).not.toBeInTheDocument()
|
||||
expect(screen.queryByRole('button', { name: 'Stop DSH service' })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('retains controls and reports stop errors for retry', async () => {
|
||||
api.getRuntimeNativeClient.mockResolvedValue({ serviceId: 'service' })
|
||||
api.stopRuntimeNativeClient.mockRejectedValueOnce(new Error('Stop failed'))
|
||||
render(<RuntimeNativeClientActions {...props} browser />)
|
||||
fireEvent.click(await screen.findByRole('button', { name: 'Stop this service' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Stop service' }))
|
||||
fireEvent.click(await screen.findByRole('button', { name: 'Stop DSH service' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Confirm' }))
|
||||
await waitFor(() => {
|
||||
expect(props.notify).toHaveBeenCalledWith({ tone: 'error', message: 'Stop failed' })
|
||||
expect(screen.getByRole('button', { name: 'Stop service' })).toBeEnabled()
|
||||
expect(screen.getByRole('button', { name: 'Confirm' })).toBeEnabled()
|
||||
})
|
||||
})
|
||||
|
||||
it('cancels inline confirmation and shows stopping on the confirmation button', async () => {
|
||||
await changeUiLocale('zh-CN')
|
||||
api.getRuntimeNativeClient.mockResolvedValue({ serviceId: 'service' })
|
||||
let finish!: () => void
|
||||
api.stopRuntimeNativeClient.mockImplementation(() => new Promise(resolve => { finish = resolve }))
|
||||
render(<RuntimeNativeClientActions {...props} browser />)
|
||||
fireEvent.click(await screen.findByRole('button', { name: '停止DSH服务' }))
|
||||
expect(screen.getByRole('button', { name: '确认' })).toHaveAccessibleDescription('停止后此服务的网页会断开,进行中的请求将中断。')
|
||||
fireEvent.click(screen.getByRole('button', { name: '取消' }))
|
||||
expect(screen.queryByRole('button', { name: '确认' })).not.toBeInTheDocument()
|
||||
expect(api.stopRuntimeNativeClient).not.toHaveBeenCalled()
|
||||
fireEvent.click(screen.getByRole('button', { name: '停止DSH服务' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: '确认' }))
|
||||
expect(screen.getByRole('button', { name: '正在停止…' })).toBeDisabled()
|
||||
expect(screen.getByRole('button', { name: '打开DSH Web' })).toBeDisabled()
|
||||
expect(screen.getByRole('button', { name: '取消' })).toBeDisabled()
|
||||
await act(async () => finish())
|
||||
expect(screen.getByRole('button', { name: '打开DSH Web' })).toBeEnabled()
|
||||
expect(screen.queryByRole('button', { name: '取消' })).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -86,14 +86,16 @@ export function RuntimeNativeClientActions({ browser, contextKey, conversationId
|
||||
|
||||
return <>
|
||||
<button className="composer-context-compact" type="button" disabled={!!pending} aria-busy={!!pending} onClick={() => void run()}>
|
||||
{pending ? <LoaderCircle aria-hidden="true" className="context-chip__spinner" size={13} /> : browser ? <Globe aria-hidden="true" size={13} /> : <TerminalSquare aria-hidden="true" size={13} />}
|
||||
{t(pending === 'stop' ? 'composer.nativeClient.stopping' : pending ? 'composer.nativeClient.opening' : serviceId ? 'composer.nativeClient.reopen' : browser ? 'composer.nativeClient.browser' : 'composer.nativeClient.terminal')}
|
||||
{pending === 'open' ? <LoaderCircle aria-hidden="true" className="context-chip__spinner" size={13} /> : browser ? <Globe aria-hidden="true" size={13} /> : <TerminalSquare aria-hidden="true" size={13} />}
|
||||
{t(pending === 'open' ? 'composer.nativeClient.opening' : browser ? 'composer.nativeClient.browser' : 'composer.nativeClient.terminal')}
|
||||
</button>
|
||||
{browser && serviceId && <button className="composer-context-compact danger-ghost" type="button" disabled={!!pending} onClick={() => setConfirmStop(serviceId)}>{t('composer.nativeClient.stop')}</button>}
|
||||
{serviceId && confirmStop === serviceId && <span className="composer-native-client-confirm" role="group" aria-label={t('composer.nativeClient.stop')}>
|
||||
<span>{t('composer.nativeClient.stopWarning')}</span>
|
||||
<button type="button" className="secondary-button" disabled={!!pending} onClick={() => setConfirmStop(undefined)}>{t('composer.nativeClient.cancel')}</button>
|
||||
<button type="button" className="danger-ghost" disabled={!!pending} onClick={() => void run(serviceId)}>{t('composer.nativeClient.confirmStop')}</button>
|
||||
<button type="button" className="composer-context-compact danger-ghost" title={t('composer.nativeClient.stopWarning')} aria-description={t('composer.nativeClient.stopWarning')} aria-busy={pending === 'stop'} disabled={!!pending} onClick={() => void run(serviceId)}>
|
||||
{pending === 'stop' && <LoaderCircle aria-hidden="true" className="context-chip__spinner" size={13} />}
|
||||
{t(pending === 'stop' ? 'composer.nativeClient.stopping' : 'composer.nativeClient.confirmStop')}
|
||||
</button>
|
||||
<button type="button" className="composer-context-compact" disabled={!!pending} onClick={() => setConfirmStop(undefined)}>{t('composer.nativeClient.cancel')}</button>
|
||||
</span>}
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -631,8 +631,8 @@ export const app = {
|
||||
compactFailed: 'Context compaction failed'
|
||||
},
|
||||
nativeClient: {
|
||||
terminal: 'Open in terminal', browser: 'Open in browser', opening: 'Opening...', stopping: 'Stopping...',
|
||||
reopen: 'Reopen', stop: 'Stop this service', confirmStop: 'Stop service', cancel: 'Cancel',
|
||||
terminal: 'Open in terminal', browser: 'Open DSH Web', opening: 'Opening...', stopping: 'Stopping...',
|
||||
stop: 'Stop DSH service', confirmStop: 'Confirm', cancel: 'Cancel',
|
||||
description: 'Independent session. Closing the page keeps the service running; stop it here or quit GoodBuddy.',
|
||||
stopWarning: 'Stopping disconnects this service and interrupts its requests.',
|
||||
stopped: 'Native client service stopped', failed: 'Could not open the native client'
|
||||
|
||||
@@ -607,8 +607,8 @@ export const app = {
|
||||
compactFailed: '上下文压缩失败'
|
||||
},
|
||||
nativeClient: {
|
||||
terminal: '在终端中打开', browser: '在浏览器中打开', opening: '正在打开…', stopping: '正在停止…',
|
||||
reopen: '重新打开', stop: '停止此服务', confirmStop: '停止服务', cancel: '取消',
|
||||
terminal: '在终端中打开', browser: '打开DSH Web', opening: '正在打开…', stopping: '正在停止…',
|
||||
stop: '停止DSH服务', confirmStop: '确认', cancel: '取消',
|
||||
description: '独立会话;关闭网页后服务仍运行,可在此停止,退出 GoodBuddy 时停止。',
|
||||
stopWarning: '停止后此服务的网页会断开,进行中的请求将中断。',
|
||||
stopped: '原生客户端服务已停止', failed: '无法打开原生客户端'
|
||||
|
||||
@@ -8871,19 +8871,23 @@ button > svg {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.composer-native-client-note,
|
||||
.composer-native-client-confirm {
|
||||
.composer-native-client-note {
|
||||
flex-basis: 100%;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.composer-native-client-confirm {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
display: inline-flex;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.composer-native-client-confirm .composer-context-compact {
|
||||
min-width: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.composer-meta__error {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user