Merge remote-tracking branch 'origin/master' into rewrite/session-export-on-master

This commit is contained in:
NI0317
2026-08-12 20:18:04 +08:00
24 changed files with 377 additions and 19 deletions

View File

@@ -244,6 +244,12 @@
font: var(--dsw-font-markdown-code-block-small);
}
.maxTokensTitle {
margin-right: 6px;
color: var(--dsw-alias-state-warn-primary);
font-weight: 600;
}
@keyframes retry-shimmer {
from {
background-position: 100% 50%;

View File

@@ -130,6 +130,21 @@ function TurnErrorItem({ node, t }: {
)
}
/** Persistent, turn-positioned notice for a turn ended at the output-token cap. */
function TurnMaxTokensItem({ t }: {
t: ChatViewSlotProps['t']
}) {
return (
<div className={css.turnErrorRow} role="status">
<StateDot state="warning" className={css.turnErrorDot} />
<div className={css.turnErrorCopy}>
<span className={css.maxTokensTitle}>{t('message.maxTokens')}</span>
<span className={css.turnErrorMessage}>{t('message.maxTokens.hint')}</span>
</div>
</div>
)
}
/**
* Display projection of reference forms in a user bubble (free geometry — no
* textarea alignment constraint here); everything else stays plain text. The
@@ -272,6 +287,11 @@ export const TurnErrorNodeView = memo(function TurnErrorNodeView({ node, t }: Ch
return <TurnErrorItem node={node.data} t={t} />
})
/** Max-tokens turn-end notice keyed Chat renderer. */
export const TurnMaxTokensNodeView = memo(function TurnMaxTokensNodeView({ t }: ChatNodeViewProps<'turn-max-tokens'>) {
return <TurnMaxTokensItem t={t} />
})
/** Explicit unknown-surface keyed Chat renderer. */
export const UnknownNodeView = memo(function UnknownNodeView({ node, t }: ChatNodeViewProps<'unknown'>) {
const data = node.data

View File

@@ -4,7 +4,7 @@ import { AssistantNodeView } from './AssistantNodeView.tsx'
import { CommandNodeView, ManualCompactionNodeView } from './CommandNodeView.tsx'
import {
CompactionNodeView, ContextMessageNodeView, RetryNodeView, TurnErrorNodeView,
UnknownNodeView, UserMessageNodeView,
TurnMaxTokensNodeView, UnknownNodeView, UserMessageNodeView,
} from './MessageItem.tsx'
import { TurnTailNodeView } from './TurnTailNodeView.tsx'
@@ -35,6 +35,8 @@ export function registerChatNodeRenderers(ctx: Context): void {
{ name: 'conversation.chat.node', key: 'model-retry', locale: NS }, RetryNodeView))
ctx.slots.inject('conversation.chat.node', () => ctx.slots.register(
{ name: 'conversation.chat.node', key: 'turn-error', locale: NS }, TurnErrorNodeView))
ctx.slots.inject('conversation.chat.node', () => ctx.slots.register(
{ name: 'conversation.chat.node', key: 'turn-max-tokens', locale: NS }, TurnMaxTokensNodeView))
ctx.slots.inject('conversation.chat.node', () => ctx.slots.register({
name: 'conversation.chat.node',
key: 'turn-tail',

View File

@@ -165,6 +165,7 @@ function legacyContribution(raw: ChatConversationViewNode): LegacyContribution {
case 'command':
case 'compaction':
case 'turn-error':
case 'turn-max-tokens':
case 'unknown':
return { anchorSeq: node.anchorSeq, nodes: [node.data], partial: null, running: null }
case 'assistant-step': {

View File

@@ -7,11 +7,14 @@ import type {
/**
* Relative positions in one durable event's seq neighborhood: interrupted
* Assistant, its follow-up Nodes, then follow-ups to an ordinary final.
* Assistant, its follow-up Nodes, then follow-ups to an ordinary final. The
* max-tokens notice sits between a closing Assistant and the turn-tail so the
* tail stays the turn's last node and keeps its branch action enabled.
*/
export const CHAT_SYNTHETIC_SEQ_OFFSETS = {
interruptedAssistant: -0.9,
interruptedFollowup: -0.8,
maxTokensNotice: 0.05,
finalizedFollowup: 0.1,
} as const

View File

@@ -9,6 +9,7 @@ import { registerMessageConversationNode } from './message.ts'
import { registerRetryConversationNode } from './retry.ts'
import { registerToolConversationNode } from './tool.ts'
import { registerTurnErrorConversationNode } from './turn-error.ts'
import { registerTurnMaxTokensConversationNode } from './turn-max-tokens.ts'
import { registerTurnTailConversationNode } from './turn-tail.ts'
/**
@@ -24,6 +25,7 @@ export function registerConversationNodes(ctx: Context): void {
registerCompactionConversationNode(ctx)
registerRetryConversationNode(ctx)
registerTurnErrorConversationNode(ctx)
registerTurnMaxTokensConversationNode(ctx)
registerTurnTailConversationNode(ctx)
registerUnknownConversationFallback(ctx)
registerChatConversationView(ctx)

View File

@@ -0,0 +1,82 @@
import type { Context } from '@deepseek-ai/cordis'
import type {
ConversationMatch, ConversationNodeContext, ConversationNodeDefinition, TurnMaxTokensNode,
} from '@deepseek-ai/dsh-client-runtime/client'
import { CHAT_SYNTHETIC_SEQ_OFFSETS, chatNode } from './common.ts'
declare module '@deepseek-ai/dsh-client-ui-conversation/client' {
interface ChatNodeDataMap {
/** Turn ended by the per-request output-token cap. */
'turn-max-tokens': TurnMaxTokensNode
}
}
interface TurnMaxTokensState {
readonly turn: number
readonly seq: number
readonly time: number
}
function lastStep(context: ConversationNodeContext<TurnMaxTokensState>): number {
const location = context.start?.location ?? context.matches[0]?.location
if (location?.kind !== 'turn' && location?.kind !== 'step') return 0
return location.turn.steps.at(-1)?.step ?? 0
}
/**
* Anchor the notice between the closing Assistant and the turn-tail so the
* tail stays the turn's last Chat node and keeps its branch action enabled.
* Without a closing text Assistant there is no branch action to protect, and
* the turn/end seq keeps the notice at the truncation point.
*/
function noticeAnchor(context: ConversationNodeContext<TurnMaxTokensState>, seq: number): number {
const location = context.start?.location ?? context.matches[0]?.location
if (location?.kind !== 'turn' && location?.kind !== 'step') return seq
const closing = location.turn.data.get('turn-tail')?.closing
return closing === null || closing === undefined
? seq
: closing.finalNode.seq + CHAT_SYNTHETIC_SEQ_OFFSETS.maxTokensNotice
}
function stateFrom(match: ConversationMatch): TurnMaxTokensState | undefined {
if (match.event.type !== 'turn/end' || match.event.data.reason.kind !== 'max-tokens') return undefined
return { turn: match.event.data.turn, seq: match.event.seq, time: match.event.time }
}
/** Notice Definition for a turn the provider ended at its output-token cap. */
export const turnMaxTokensDefinition: ConversationNodeDefinition<TurnMaxTokensState> = {
kind: 'turn-max-tokens',
target: 'chat',
match: (event) => {
if (event.type === 'turn/end' && event.data.reason.kind === 'max-tokens') {
return { id: String(event.data.turn), role: 'start' }
}
return null
},
start: (_context, match) => {
const state = stateFrom(match)
if (state === undefined) throw new Error('turn-max-tokens start requires a max-tokens turn/end')
return state
},
update: context => context.state,
buildViewNode: (context) => {
const state = context.state
if (state === undefined) return null
const node: TurnMaxTokensNode = {
kind: 'turn-max-tokens',
seq: state.seq,
time: state.time,
turn: state.turn,
step: lastStep(context),
}
return chatNode(context, 'turn-max-tokens', noticeAnchor(context, state.seq), node)
},
}
/**
* Register the max-tokens turn-end notice contribution.
* @param ctx - owning UI Conversation context.
*/
export function registerTurnMaxTokensConversationNode(ctx: Context): void {
ctx.conversationEvents.register(turnMaxTokensDefinition)
}

View File

@@ -11,6 +11,7 @@ export type {} from './conversation-nodes/message.ts'
export type {} from './conversation-nodes/retry.ts'
export type {} from './conversation-nodes/tool.ts'
export type {} from './conversation-nodes/turn-error.ts'
export type {} from './conversation-nodes/turn-max-tokens.ts'
export type {} from './conversation-nodes/turn-tail.ts'
export { apply, inject } from './apply.ts'

View File

@@ -122,6 +122,8 @@ export const zh = {
'message.retry.delay': '重试延迟:',
'message.retry.failure': '失败原因:',
'message.turnError': '本轮运行失败',
'message.maxTokens': '已达到输出 token 上限',
'message.maxTokens.hint': '回答被截断,已有输出保留在对话中。发送“继续”可让模型接着输出。',
'message.ranFor': '用时 {duration}',
'message.ttft': '首 token {seconds}秒',
'message.tokensPerSecond': '{tps} tok/s',
@@ -289,6 +291,8 @@ export const en = {
'message.retry.delay': 'Retry delay: ',
'message.retry.failure': 'Failure reason: ',
'message.turnError': 'This turn failed',
'message.maxTokens': 'Output token limit reached',
'message.maxTokens.hint': 'The reply was cut off; earlier output is preserved in the conversation. Send "continue" to let the model resume.',
'message.ranFor': 'Ran for {duration}',
'message.ttft': 'TTFT {seconds}s',
'message.tokensPerSecond': '{tps} tok/s',