fix(ui-chat): show a stopped notice for turns interrupted by a DSH stop
This commit is contained in:
1 parent
d83cdfafd6
commit
3121a4b5da
5 files changed
+48
-11
No files matched your search
@@ -53,6 +53,7 @@ function failureMessage(
|
||||
t: ChatViewSlotProps['t'],
|
||||
): string {
|
||||
if (code === 'ACCOUNT_SIGNED_OUT') return t('message.failure.accountSignedOut')
|
||||
if (code === 'INTERRUPTED') return t('message.failure.interrupted')
|
||||
if (code === 'ACCOUNT_SIGN_IN_REQUIRED') return t('message.failure.accountSignInRequired')
|
||||
if (code === 'QUOTA' || code === 'ACCOUNT_QUOTA') return t('message.failure.quota')
|
||||
return code === 'AUTH' ? t('message.failure.auth') : message
|
||||
@@ -135,7 +136,7 @@ function TurnErrorItem({ node, t }: {
|
||||
<div className={css.turnErrorRow} role="status">
|
||||
<StateDot state="error" className={css.turnErrorDot} />
|
||||
<div className={css.turnErrorCopy}>
|
||||
<span className={css.turnErrorTitle}>{node.code === 'ACCOUNT_SIGNED_OUT' ? t('message.accountStopped') : t('message.turnError')}</span>
|
||||
<span className={css.turnErrorTitle}>{node.code === 'ACCOUNT_SIGNED_OUT' || node.code === 'INTERRUPTED' ? t('message.taskStopped') : t('message.turnError')}</span>
|
||||
<span className={css.turnErrorMessage}>{failureMessage(node.message, node.code, t)}</span>
|
||||
</div>
|
||||
{node.code !== undefined && <code className={css.turnErrorCode}>{node.code}</code>}
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { Context } from '@deepseek-ai/cordis'
|
||||
import type {
|
||||
ConversationMatch, ConversationNodeContext, ConversationNodeDefinition, TurnErrorNode,
|
||||
} from '@deepseek-ai/dsh-client-ui-conversation/client'
|
||||
import type { TurnEndReason } from '@deepseek-ai/dsh-session/types'
|
||||
import { chatNode } from './common.ts'
|
||||
import { displayFailure } from './event-projection.ts'
|
||||
|
||||
@@ -28,13 +29,30 @@ function lastStep(context: ConversationNodeContext<TurnErrorState>): number {
|
||||
return location.turn.steps.at(-1)?.step ?? 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Failure shown for a turn ending: a recorded error, a sign-out cancellation,
|
||||
* or a turn closed after DSH stopped running mid-turn.
|
||||
*/
|
||||
function endFailure(reason: TurnEndReason): { readonly message: string; readonly code?: string } | undefined {
|
||||
switch (reason.kind) {
|
||||
case 'error':
|
||||
return reason.error
|
||||
case 'aborted':
|
||||
return reason.reason.kind === 'hook' && reason.reason.reason === 'deepseek-account/signed-out'
|
||||
? { 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' }
|
||||
default:
|
||||
// Completed, blocked, max-tokens, forked, user/parent cancellation, and
|
||||
// plugin-merged endings have no failure row.
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
function failureFrom(match: ConversationMatch): TurnErrorState['failure'] | undefined {
|
||||
if (match.event.type !== 'turn/end') return undefined
|
||||
const reason = match.event.data.reason
|
||||
const failure = reason.kind === 'error' ? reason.error
|
||||
: reason.kind === 'aborted' && reason.reason.kind === 'hook'
|
||||
&& reason.reason.reason === 'deepseek-account/signed-out'
|
||||
? { message: 'Stopped because you signed out of DeepSeek.', code: 'ACCOUNT_SIGNED_OUT' } : undefined
|
||||
const failure = endFailure(match.event.data.reason)
|
||||
if (failure === undefined) return undefined
|
||||
const display = displayFailure(failure)
|
||||
return {
|
||||
@@ -63,9 +81,7 @@ export const turnErrorDefinition: ConversationNodeDefinition<TurnErrorState> = {
|
||||
target: 'chat',
|
||||
match: (event) => {
|
||||
if (event.type === 'turn/start') return { id: String(event.data.turn), role: 'start' }
|
||||
if (event.type === 'turn/end' && (event.data.reason.kind === 'error'
|
||||
|| (event.data.reason.kind === 'aborted' && event.data.reason.reason.kind === 'hook'
|
||||
&& event.data.reason.reason.reason === 'deepseek-account/signed-out'))) {
|
||||
if (event.type === 'turn/end' && endFailure(event.data.reason) !== undefined) {
|
||||
return { id: String(event.data.turn), role: 'update' }
|
||||
}
|
||||
return null
|
||||
|
||||
@@ -161,8 +161,9 @@ export const zh = {
|
||||
'message.retry.delay': '重试延迟:',
|
||||
'message.retry.failure': '失败原因:',
|
||||
'message.failure.auth': 'API 密钥无效',
|
||||
'message.accountStopped': '任务已停止',
|
||||
'message.taskStopped': '任务已停止',
|
||||
'message.failure.accountSignedOut': '任务已因退出 DeepSeek 登录而停止。',
|
||||
'message.failure.interrupted': 'DSH 在本轮完成前停止运行,未完成的工具调用结果未知。',
|
||||
'message.failure.accountSignInRequired': '请先登录 DeepSeek,并确认请求地址支持账号认证。',
|
||||
'message.failure.quota': '当前请求的额度已用尽',
|
||||
'message.turnError': '本轮运行失败',
|
||||
@@ -354,8 +355,9 @@ export const en = {
|
||||
'message.retry.delay': 'Retry delay: ',
|
||||
'message.retry.failure': 'Failure reason: ',
|
||||
'message.failure.auth': 'API key is invalid',
|
||||
'message.accountStopped': 'Task stopped',
|
||||
'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.accountSignInRequired': 'Sign in to DeepSeek and ensure the request destination supports account authentication.',
|
||||
'message.failure.quota': 'Request quota exhausted.',
|
||||
'message.turnError': 'This turn failed',
|
||||
|
||||
@@ -2128,6 +2128,7 @@ describe('ChatView', () => {
|
||||
it('renders every terminal failure inline with neutral quota copy and no transient notice', () => {
|
||||
const h = makeHarness({ nodes: [
|
||||
user(1, 'try'), turnError(2, 'AUTH'), turnError(3), turnError(4, 'QUOTA'), turnError(5, 'ACCOUNT_QUOTA'),
|
||||
turnError(6, 'INTERRUPTED'),
|
||||
] })
|
||||
const view = render(<h.ChatView {...h.props} />)
|
||||
const statuses = view.getAllByRole('status')
|
||||
@@ -2136,6 +2137,7 @@ describe('ChatView', () => {
|
||||
'本轮运行失败plugin exploded',
|
||||
'本轮运行失败当前请求的额度已用尽QUOTA',
|
||||
'本轮运行失败当前请求的额度已用尽ACCOUNT_QUOTA',
|
||||
'任务已停止DSH 在本轮完成前停止运行,未完成的工具调用结果未知。INTERRUPTED',
|
||||
])
|
||||
// The transient notice is the frame-wide host's job; the failure row keeps
|
||||
// neither a recharge affordance nor a toast of its own.
|
||||
|
||||
@@ -2800,3 +2800,19 @@ it('retains a sign-out cancellation notice when reopening a partial turn', () =>
|
||||
code: 'ACCOUNT_SIGNED_OUT', message: 'Stopped because you signed out of DeepSeek.',
|
||||
})
|
||||
})
|
||||
|
||||
it('reports a turn closed after its process exited as a stopped turn', () => {
|
||||
const value = assembler([
|
||||
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.',
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps user cancellations and forked endings free of a failure row', () => {
|
||||
for (const reason of [{ kind: 'aborted', reason: { kind: 'user' } }, { kind: 'forked' }] as const) {
|
||||
const value = assembler([at(7, 'turn/end', { turn: 1, reason })], true)
|
||||
expect(node(snapshot(value), 'turn-error')).toBeUndefined()
|
||||
}
|
||||
})
|
||||
Reference in new issue
Block a user