From 2fdb9870df7ca2e94d1db821e183a7b0742e9e32 Mon Sep 17 00:00:00 2001 From: Turtle Date: Thu, 8 Oct 2026 12:11:48 +0800 Subject: [PATCH] fix(ui-chat): explain the interrupted stop in localized copy without the code chip --- apps/web/tests/interrupted-turn.e2e.ts | 4 ++-- packages/client/ui-chat/README.i18n.yaml | 4 ++-- packages/client/ui-chat/README.md | 2 +- packages/client/ui-chat/README.zh.md | 2 +- packages/client/ui-chat/src/client/chat/MessageItem.tsx | 2 +- .../ui-chat/src/client/conversation-nodes/turn-error.ts | 4 ++-- packages/client/ui-chat/src/client/locale.ts | 4 ++-- packages/client/ui-chat/tests/chat-view.client.spec.tsx | 2 +- .../tests/conversation-node-definitions.client.spec.ts | 2 +- snapshots/web/interrupted-turn/ui.expected.md | 4 +--- 10 files changed, 14 insertions(+), 16 deletions(-) diff --git a/apps/web/tests/interrupted-turn.e2e.ts b/apps/web/tests/interrupted-turn.e2e.ts index ba78f2cfc9..a680cb12bd 100644 --- a/apps/web/tests/interrupted-turn.e2e.ts +++ b/apps/web/tests/interrupted-turn.e2e.ts @@ -60,8 +60,8 @@ describe.skipIf(MODE === 'record')('web e2e: turn interrupted by a DSH stop', () onTestFailed(() => saveFailureShot(page, 'web-e2e-interrupted-turn')) const notice = page.getByRole('status').filter({ hasText: 'Task stopped' }) await notice.waitFor({ timeout: 15_000 }) - expect(await notice.textContent()).toContain('DSH stopped running before this turn finished.') - expect(await notice.textContent()).toContain('INTERRUPTED') + expect(await notice.textContent()).toContain('The DSH process exited before this turn finished') + expect(await notice.textContent()).not.toContain('INTERRUPTED') expect(await page.locator('[data-turn-process="1"]').textContent()).toBe('Stopped') const snapshot = (await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd)) diff --git a/packages/client/ui-chat/README.i18n.yaml b/packages/client/ui-chat/README.i18n.yaml index 91c230be23..1c4f806561 100644 --- a/packages/client/ui-chat/README.i18n.yaml +++ b/packages/client/ui-chat/README.i18n.yaml @@ -21,8 +21,8 @@ en: cbf1f0a2205046a5 zh: 2e59d0815351349f /deepseek-ai-dsh-client-ui-chat/command-and-failure-rows: - en: 2157fcd6b0d100b5 - zh: 9e865ac3953ebe30 + en: 41dcd70e14ebe9cc + zh: ededead572b59037 /deepseek-ai-dsh-client-ui-chat/turn-token-usage: en: 14343dd14e493f4b zh: 7cc7a25d05ea798f diff --git a/packages/client/ui-chat/README.md b/packages/client/ui-chat/README.md index 02bcbe3ddb..bd1ed2ef31 100644 --- a/packages/client/ui-chat/README.md +++ b/packages/client/ui-chat/README.md @@ -49,7 +49,7 @@ When an Assistant attempt retires without a visible message, Chat hides its alre ## Command and failure rows -Generic command rows retain the ordinary command glyph in every lifecycle state; failure remains explicit through the row state and summary. Every terminal Turn failure renders its inline red-dot row; a quota failure's row states the neutral `message.failure.quota` copy instead of the provider message. The transient notice for a newly appended `QUOTA` or `ACCOUNT_QUOTA` comes from this package's frame-wide entry in `shell.overlay`, which outlives the Chat panel: it offers the one live notice to the `shell.quota-notice` chain and falls back to its own warning Toast, while an entry that claims the code replaces that fallback. Only Sessions this Client has bound and materialized publish; quota failures in Sessions it never opened do not. A newer notice replaces the current one unless a claiming entry retains it with `keepOpen()`: that call returns a release the caller owns and must run on unmount, any live hold keeps the claiming entry mounted and drops later notices, and releasing resumes later notices without replaying the dropped ones. The fallback Toast has no deferral of its own: while the Desktop account's opaque native Platform page covers the document, it still runs underneath and its display timer may elapse unseen, dismissing the notice itself, so only the persistent failure row remains. A release drops only its own hold, so one that runs after a dismissal or a newer hold leaves that newer hold intact. Dismissal and sign-out clear every hold, and dropped notices are not queued while their persistent failure rows still render. History replacement and pagination never publish a notice. Intermediate retries do not create a terminal row; output-token limits use the amber warning dot. +Generic command rows retain the ordinary command glyph in every lifecycle state; failure remains explicit through the row state and summary. Every terminal Turn failure renders its inline red-dot row; a quota failure's row states the neutral `message.failure.quota` copy instead of the provider message. The transient notice for a newly appended `QUOTA` or `ACCOUNT_QUOTA` comes from this package's frame-wide entry in `shell.overlay`, which outlives the Chat panel: it offers the one live notice to the `shell.quota-notice` chain and falls back to its own warning Toast, while an entry that claims the code replaces that fallback. Only Sessions this Client has bound and materialized publish; quota failures in Sessions it never opened do not. A newer notice replaces the current one unless a claiming entry retains it with `keepOpen()`: that call returns a release the caller owns and must run on unmount, any live hold keeps the claiming entry mounted and drops later notices, and releasing resumes later notices without replaying the dropped ones. The fallback Toast has no deferral of its own: while the Desktop account's opaque native Platform page covers the document, it still runs underneath and its display timer may elapse unseen, dismissing the notice itself, so only the persistent failure row remains. A release drops only its own hold, so one that runs after a dismissal or a newer hold leaves that newer hold intact. Dismissal and sign-out clear every hold, and dropped notices are not queued while their persistent failure rows still render. History replacement and pagination never publish a notice. Intermediate retries do not create a terminal row; output-token limits use the amber warning dot. A Turn that restart recovery closed as `interrupted` renders a localized stopped row stating that the DSH process exited mid-Turn, without a code chip, and its process summary reads Stopped. ----- diff --git a/packages/client/ui-chat/README.zh.md b/packages/client/ui-chat/README.zh.md index 560e37e1fa..fc29e70b6e 100644 --- a/packages/client/ui-chat/README.zh.md +++ b/packages/client/ui-chat/README.zh.md @@ -49,7 +49,7 @@ Assistant 尝试结束且没有可见消息时,Chat 隐藏已发布的 Node, ## 指令与失败行 -通用指令行在所有生命周期状态中都保留普通指令图标;失败仍通过行状态与摘要表达。每个终止轮次都渲染自己的红点行;欠费失败的行使用中立的 `message.failure.quota` 文案,而不是提供方消息。新追加的 `QUOTA` 或 `ACCOUNT_QUOTA` 产生的瞬时提示来自本包注册在 `shell.overlay` 的全局条目,它不随 Chat 面板消失:该条目把唯一一条实时提示交给 `shell.quota-notice` 链,并在无人接管时回退到自己的警告 Toast,接管该错误码的条目会替换该回退。只有本 Client 已绑定并已物化的 Session 会发布提示;它从未打开的 Session 中的欠费不会发布。更新的提示会替换当前提示,除非认领条目用 `keepOpen()` 保持住当前提示:该调用返回一个由调用方持有、必须在卸载时调用的释放函数,只要存在保持,认领条目就继续挂载,后续提示被丢弃,释放后恢复后续提示但不重放已丢弃的提示。回退 Toast 自身没有延后策略:当 Desktop 账号的不透明原生 Platform 页盖住本页时,它仍会在下方运行,其显示计时器可能在不可见时走完并自行撤下提示,因此只留下持久化失败行。释放只移除自己的保持,因此它在关闭或更新的保持之后执行时不会影响更新的保持。关闭与退出登录会清空所有保持;被丢弃的提示不排队,其对应的持久化失败行仍会渲染。历史替换和分页不会发布提示。中间重试不会创建终止行;输出 token 上限使用琥珀色警告点。 +通用指令行在所有生命周期状态中都保留普通指令图标;失败仍通过行状态与摘要表达。每个终止轮次都渲染自己的红点行;欠费失败的行使用中立的 `message.failure.quota` 文案,而不是提供方消息。新追加的 `QUOTA` 或 `ACCOUNT_QUOTA` 产生的瞬时提示来自本包注册在 `shell.overlay` 的全局条目,它不随 Chat 面板消失:该条目把唯一一条实时提示交给 `shell.quota-notice` 链,并在无人接管时回退到自己的警告 Toast,接管该错误码的条目会替换该回退。只有本 Client 已绑定并已物化的 Session 会发布提示;它从未打开的 Session 中的欠费不会发布。更新的提示会替换当前提示,除非认领条目用 `keepOpen()` 保持住当前提示:该调用返回一个由调用方持有、必须在卸载时调用的释放函数,只要存在保持,认领条目就继续挂载,后续提示被丢弃,释放后恢复后续提示但不重放已丢弃的提示。回退 Toast 自身没有延后策略:当 Desktop 账号的不透明原生 Platform 页盖住本页时,它仍会在下方运行,其显示计时器可能在不可见时走完并自行撤下提示,因此只留下持久化失败行。释放只移除自己的保持,因此它在关闭或更新的保持之后执行时不会影响更新的保持。关闭与退出登录会清空所有保持;被丢弃的提示不排队,其对应的持久化失败行仍会渲染。历史替换和分页不会发布提示。中间重试不会创建终止行;输出 token 上限使用琥珀色警告点。重启恢复以 `interrupted` 结束的轮次会渲染本地化的停止行,说明 DSH 进程在轮次中途退出,不显示代码标签,其过程摘要显示“已停止”。 ----- diff --git a/packages/client/ui-chat/src/client/chat/MessageItem.tsx b/packages/client/ui-chat/src/client/chat/MessageItem.tsx index c45ca736cd..078cd9295f 100644 --- a/packages/client/ui-chat/src/client/chat/MessageItem.tsx +++ b/packages/client/ui-chat/src/client/chat/MessageItem.tsx @@ -139,7 +139,7 @@ function TurnErrorItem({ node, t }: { {node.code === 'ACCOUNT_SIGNED_OUT' || node.code === 'INTERRUPTED' ? t('message.taskStopped') : t('message.turnError')} {failureMessage(node.message, node.code, t)} - {node.code !== undefined && {node.code}} + {node.code !== undefined && node.code !== 'INTERRUPTED' && {node.code}} ) } diff --git a/packages/client/ui-chat/src/client/conversation-nodes/turn-error.ts b/packages/client/ui-chat/src/client/conversation-nodes/turn-error.ts index d7c260d92c..a27cce8b80 100644 --- a/packages/client/ui-chat/src/client/conversation-nodes/turn-error.ts +++ b/packages/client/ui-chat/src/client/conversation-nodes/turn-error.ts @@ -31,7 +31,7 @@ function lastStep(context: ConversationNodeContext): number { /** * Failure shown for a turn ending: a recorded error, a sign-out cancellation, - * or a turn closed after DSH stopped running mid-turn. + * or a turn that restart recovery closed because the DSH process exited mid-turn. */ function endFailure(reason: TurnEndReason): { readonly message: string; readonly code?: string } | undefined { switch (reason.kind) { @@ -43,7 +43,7 @@ function endFailure(reason: TurnEndReason): { readonly message: string; readonly ? { message: 'Stopped because you signed out of DeepSeek.', code: 'ACCOUNT_SIGNED_OUT' } : undefined case 'interrupted': - return { message: 'DSH stopped running before this turn finished.', code: 'INTERRUPTED' } + return { message: 'The DSH process exited before this turn finished.', code: 'INTERRUPTED' } default: // Completed, blocked, max-tokens, forked, and plugin-merged endings have no failure row. return undefined diff --git a/packages/client/ui-chat/src/client/locale.ts b/packages/client/ui-chat/src/client/locale.ts index 7dc00f2b2b..423addce17 100644 --- a/packages/client/ui-chat/src/client/locale.ts +++ b/packages/client/ui-chat/src/client/locale.ts @@ -163,7 +163,7 @@ export const zh = { 'message.failure.auth': 'API 密钥无效', 'message.taskStopped': '任务已停止', 'message.failure.accountSignedOut': '任务已因退出 DeepSeek 登录而停止。', - 'message.failure.interrupted': 'DSH 在本轮完成前停止运行,未完成的工具调用结果未知。', + 'message.failure.interrupted': 'DSH 进程在本轮完成前退出(例如被强制结束、崩溃或关闭超时),重新启动后本轮已中止。未完成的工具调用结果未知。', 'message.failure.accountSignInRequired': '请先登录 DeepSeek,并确认请求地址支持账号认证。', 'message.failure.quota': '当前请求的额度已用尽', 'message.turnError': '本轮运行失败', @@ -357,7 +357,7 @@ export const en = { 'message.failure.auth': 'API key is invalid', 'message.taskStopped': 'Task stopped', 'message.failure.accountSignedOut': 'Stopped because you signed out of DeepSeek.', - 'message.failure.interrupted': 'DSH stopped running before this turn finished. Unfinished tool calls have unknown outcomes.', + 'message.failure.interrupted': 'The DSH process exited before this turn finished (for example, it was killed, crashed, or timed out while shutting down), so the turn was closed when DSH restarted. Unfinished tool calls have unknown outcomes.', 'message.failure.accountSignInRequired': 'Sign in to DeepSeek and ensure the request destination supports account authentication.', 'message.failure.quota': 'Request quota exhausted.', 'message.turnError': 'This turn failed', diff --git a/packages/client/ui-chat/tests/chat-view.client.spec.tsx b/packages/client/ui-chat/tests/chat-view.client.spec.tsx index e0f05c690c..740d68909d 100644 --- a/packages/client/ui-chat/tests/chat-view.client.spec.tsx +++ b/packages/client/ui-chat/tests/chat-view.client.spec.tsx @@ -2137,7 +2137,7 @@ describe('ChatView', () => { '本轮运行失败plugin exploded', '本轮运行失败当前请求的额度已用尽QUOTA', '本轮运行失败当前请求的额度已用尽ACCOUNT_QUOTA', - '任务已停止DSH 在本轮完成前停止运行,未完成的工具调用结果未知。INTERRUPTED', + '任务已停止DSH 进程在本轮完成前退出(例如被强制结束、崩溃或关闭超时),重新启动后本轮已中止。未完成的工具调用结果未知。', ]) // The transient notice is the frame-wide host's job; the failure row keeps // neither a recharge affordance nor a toast of its own. diff --git a/packages/client/ui-chat/tests/conversation-node-definitions.client.spec.ts b/packages/client/ui-chat/tests/conversation-node-definitions.client.spec.ts index 5acf13fcfc..3f3abc50e7 100644 --- a/packages/client/ui-chat/tests/conversation-node-definitions.client.spec.ts +++ b/packages/client/ui-chat/tests/conversation-node-definitions.client.spec.ts @@ -2806,7 +2806,7 @@ it('reports a turn closed after its process exited as a stopped turn', () => { at(7, 'turn/end', { turn: 1, reason: { kind: 'interrupted' } }), ], true) expect(node(snapshot(value), 'turn-error')?.data).toMatchObject({ - code: 'INTERRUPTED', message: 'DSH stopped running before this turn finished.', + code: 'INTERRUPTED', message: 'The DSH process exited before this turn finished.', }) }) diff --git a/snapshots/web/interrupted-turn/ui.expected.md b/snapshots/web/interrupted-turn/ui.expected.md index 0f55c17e6a..b1edd6081b 100644 --- a/snapshots/web/interrupted-turn/ui.expected.md +++ b/snapshots/web/interrupted-turn/ui.expected.md @@ -11,9 +11,7 @@ - button "Stopped" [disabled] [expanded] - button "Read files" - paragraph: DONE -- status: - - text: Task stoppedDSH stopped running before this turn finished. Unfinished tool calls have unknown outcomes. - - code: INTERRUPTED +- status: Task stoppedThe DSH process exited before this turn finished (for example, it was killed, crashed, or timed out while shutting down), so the turn was closed when DSH restarted. Unfinished tool calls have unknown outcomes. - button "Copy" - button "Good response" - button "Bad response"