From 585d20fc7755c24d971dda4603005d6d3efe2f66 Mon Sep 17 00:00:00 2001 From: Turtle Date: Thu, 15 Oct 2026 17:47:02 +0800 Subject: [PATCH 1/3] fix(ui-chat): pause the running clock while reconnecting The Chat running indicator kept counting while the Client connection was disconnected or reconnecting, which read as the agent still working. The flow now reads the connection state and holds the clock until the connection is back. --- packages/client/ui-chat/package.json | 2 ++ packages/client/ui-chat/src/client/apply.ts | 9 ++++-- .../ui-chat/src/client/chat/ChatFlow.tsx | 5 ++-- .../ui-chat/src/client/chat/RunningStatus.tsx | 10 ++++--- .../ui-chat/src/client/contract/slots.ts | 10 +++++-- .../conversation-nodes/README.i18n.yaml | 4 +-- .../src/client/conversation-nodes/README.md | 2 +- .../client/conversation-nodes/README.zh.md | 2 +- .../tests/apply-inject.client.spec.tsx | 3 ++ .../ui-chat/tests/chat-apply.client.spec.tsx | 1 + .../ui-chat/tests/chat-view.client.spec.tsx | 30 +++++++++++++++++-- .../tests/quota-notice.client.spec.tsx | 1 + .../tests/running-status.client.spec.tsx | 17 +++++++++-- packages/client/ui-chat/tsconfig.client.json | 3 ++ .../tests/quota-notice-hold.client.spec.tsx | 1 + .../tests/assembly-surfaces.client.spec.tsx | 1 + .../tests/chat-ptc-subcalls.client.spec.tsx | 1 + .../tests/toolview-slot.client.spec.tsx | 2 ++ pnpm-lock.yaml | 3 ++ 19 files changed, 87 insertions(+), 20 deletions(-) diff --git a/packages/client/ui-chat/package.json b/packages/client/ui-chat/package.json index d43d47801c..492b4b4b39 100644 --- a/packages/client/ui-chat/package.json +++ b/packages/client/ui-chat/package.json @@ -32,6 +32,7 @@ "inject": [ "@deepseek-ai/dsh-api-session-controller", "@deepseek-ai/dsh-api-workspace-controller", + "@deepseek-ai/dsh-client-connection", "@deepseek-ai/dsh-client-locale", "@deepseek-ai/dsh-client-ui-conversation", "@deepseek-ai/dsh-client-ui-input-trigger", @@ -62,6 +63,7 @@ "@deepseek-ai/dsh-api-workspace-files": "workspace:*", "@deepseek-ai/dsh-attachment": "workspace:*", "@deepseek-ai/dsh-brand": "workspace:*", + "@deepseek-ai/dsh-client-connection": "workspace:*", "@deepseek-ai/dsh-client-locale": "workspace:*", "@deepseek-ai/dsh-client-store": "workspace:*", "@deepseek-ai/dsh-client-ui-approval": "workspace:*", diff --git a/packages/client/ui-chat/src/client/apply.ts b/packages/client/ui-chat/src/client/apply.ts index d51a29d316..6432e3e09e 100644 --- a/packages/client/ui-chat/src/client/apply.ts +++ b/packages/client/ui-chat/src/client/apply.ts @@ -21,6 +21,7 @@ import type {} from '@deepseek-ai/dsh-client-ui-renderer/client' import type {} from '@deepseek-ai/dsh-client-ui-session/client' import type {} from '@deepseek-ai/dsh-client-ui-settings/client' import type {} from '@deepseek-ai/dsh-client-ui-workspace/client' +import type { ConnectionHandle } from '@deepseek-ai/dsh-client-connection/client' import type { ChatFlowDataInjected, ChatFlowInjected, ChatNodeInjected, ChatScrollPosition, ChatViewInjected, QuotaNoticeInjected, QuotaNoticeState, TurnTailOwnerProps, @@ -74,7 +75,7 @@ export const CHAT_NODE_INJECT: ChatNodeInjected = { /** Services required by the Chat target and its presentation registrations. */ export const inject = [ 'slots', 'sessions', 'uiWorkspace', 'uiSession', 'uiConversation', 'locale', - 'configForms', 'remote', 'remote.session', 'sidebarRight', + 'configForms', 'connection', 'remote', 'remote.session', 'sidebarRight', ] /** @@ -203,7 +204,8 @@ export function apply(ctx: Context): void { }), }, TranscriptViewRow)) - const nodeSources = (binding: SessionBinding): ChatFlowDataInjected => { + const connectionState = (ctx.get('connection') as ConnectionHandle).state + const nodeSources = (binding: SessionBinding): Pick => { const chat = chatSource(binding) const conversation = ctx.uiConversation.binding(binding) return { @@ -225,7 +227,8 @@ export function apply(ctx: Context): void { inject: (sessionId: SessionId): ChatFlowDataInjected => { const binding = ctx.sessions.binding(sessionId) if (binding === undefined) throw new Error(`ui-chat: unknown session "${sessionId}"`) - return nodeSources(binding) + const sources = nodeSources(binding) + return { ...sources, hooks: { ...sources.hooks, connectionState } } }, }, ChatFlow)) diff --git a/packages/client/ui-chat/src/client/chat/ChatFlow.tsx b/packages/client/ui-chat/src/client/chat/ChatFlow.tsx index 224d5c7a4e..561632a671 100644 --- a/packages/client/ui-chat/src/client/chat/ChatFlow.tsx +++ b/packages/client/ui-chat/src/client/chat/ChatFlow.tsx @@ -25,12 +25,13 @@ function isTranscriptEcho(item: PendingInput): boolean { /** Render the flow without adding a DOM parent or changing keyed row positions. */ export const ChatFlow = memo(function ChatFlow({ entries, pendingInputs, lastInputTurn, deferCollapse, - useSession, useChat, useChatNode, useChatNodeBottom, useChatNodeProcess, useChatGroup, usePresentation, + useSession, useChat, useChatNode, useChatNodeBottom, useChatNodeProcess, useChatGroup, usePresentation, useConnectionState, useStore, actions, renderSlot, renderFactorySlot, t, useGroupAction, useGroupHeaderAction, cwd, openFile, inspectCall, forkAt, loadImage, fileMentions, }: ChatFlowSlotProps) { const nodeStore = useChat(snapshot => snapshot.nodes) const running = useSession(snapshot => snapshot.running) + const disconnected = useConnectionState(state => state === 'disconnected' || state === 'connecting') const latestTurnAnchor = useChat(snapshot => snapshot.navigation.items().at(-1)?.anchorKey) const runningStartTime = useChatNode(latestTurnAnchor ?? '', (node) => { const location = node?.location @@ -85,7 +86,7 @@ export const ChatFlow = memo(function ChatFlow({ // Rows enter before the keyed status and echoes after it, so React never moves its DOM node. return [ ...rows, ...leadingRows, ...openingRows, - ...running ? [] : [], + ...running ? [] : [], ...pendingRows, ] }) diff --git a/packages/client/ui-chat/src/client/chat/RunningStatus.tsx b/packages/client/ui-chat/src/client/chat/RunningStatus.tsx index fd145b6324..36a202c973 100644 --- a/packages/client/ui-chat/src/client/chat/RunningStatus.tsx +++ b/packages/client/ui-chat/src/client/chat/RunningStatus.tsx @@ -9,22 +9,24 @@ import css from './ChatView.module.css' interface RunningStatusProps { readonly startTime: number | undefined + /** Freeze the shown duration while the Client cannot observe the Session. */ + readonly paused: boolean readonly t: ChatViewSlotProps['t'] } /** * Show live elapsed time after the current Turn's content without announcing ticks. - * @param props - Current Turn start time and localized copy. + * @param props - Current Turn start time, pause state, and localized copy. * @returns the blue running indicator; mount only while the Session is running. */ -export const RunningStatus = memo(function RunningStatus({ startTime, t }: RunningStatusProps) { +export const RunningStatus = memo(function RunningStatus({ startTime, paused, t }: RunningStatusProps) { const [now, setNow] = useState(Date.now) useEffect(() => { - if (startTime === undefined) return + if (startTime === undefined || paused) return setNow(Date.now()) const timer = setInterval(() => { setNow(Date.now()) }, LIVE_RUN_CLOCK_INTERVAL_MS) return () => { clearInterval(timer) } - }, [startTime]) + }, [startTime, paused]) const label = startTime === undefined ? t('chat.deepDiving') : t('chat.deepDivingFor', { duration: formatRunDuration(Math.max(1000, now - startTime), t).map(part => part.text).join(''), }) diff --git a/packages/client/ui-chat/src/client/contract/slots.ts b/packages/client/ui-chat/src/client/contract/slots.ts index a85e28798d..4bd88840f6 100644 --- a/packages/client/ui-chat/src/client/contract/slots.ts +++ b/packages/client/ui-chat/src/client/contract/slots.ts @@ -15,6 +15,7 @@ import type { PropsRenderFactories, PropsStore, SlotHookFactory, SnapshotSelectorHook, } from '@deepseek-ai/dsh-client-ui-slots' import type { ObservableSnapshot } from '@deepseek-ai/dsh-client-store' +import type { ConnectionStateSource } from '@deepseek-ai/dsh-client-connection/client' import type { MarkdownFileMentions, MarkdownLabels } from '@deepseek-ai/dsh-client-ui-primitives' import type {} from '@deepseek-ai/dsh-client-ui-layout/client' // Type-only: the Session-scope access Hook (`useAccess`) the Chat entries read. @@ -325,8 +326,13 @@ export interface ChatViewInjected { fileMentions: (owner: TurnTailOwnerProps) => MarkdownFileMentions | undefined } -/** Existing Session-owned node sources shared by the view and its flow renderer. */ -export type ChatFlowDataInjected = Pick +/** Session-owned node sources shared with the view, plus the flow's connection state. */ +export type ChatFlowDataInjected = Pick & { + hooks: ChatViewInjected['hooks'] & { + /** Client connection lifecycle; the running clock pauses while it is not connected. */ + connectionState: ConnectionStateSource + } +} /** Ordered rows and reconciled local echoes rendered inside one Chat viewport. */ export interface ChatFlowOwnerProps extends Pick