diff --git a/packages/client/ui-chat/src/client/chat/ChatView.module.css b/packages/client/ui-chat/src/client/chat/ChatView.module.css index cf0aabc7a5..086dc40ee4 100644 --- a/packages/client/ui-chat/src/client/chat/ChatView.module.css +++ b/packages/client/ui-chat/src/client/chat/ChatView.module.css @@ -101,13 +101,55 @@ line-height: calc(18px + var(--dsh-content-font-delta-secondary, 0px)); } -/* Decorative whale below the last flow row while the Session runs. */ -.runningWhale { +.running { + display: flex; + flex-direction: column; + align-items: flex-start; + --dsh-chat-flow-gap: 12px; + color: var(--dsw-alias-label-deep-diving); + font-size: calc(var(--dsh-content-font-size, 14px) - 2px); + line-height: calc(22px + var(--dsh-content-font-delta, 0px)); +} + +/* The last visible row before the status must contain output, not an input. + Hidden and empty seats do not separate a user message from the status. */ +.column > :nth-last-child(2 of :not([hidden]):not(.flowItem:is(:empty, :has(> [data-slot="conversation.chat.node"]:empty)))):is( + .flowItem:not([data-chat-flow-kind="user"]):not([data-chat-flow-kind="steering"]):not([data-chat-flow-kind="turn-trigger"]), + [data-chat-group-key] +) ~ .running > .runningDivider { + display: block; +} + +.runningDivider { + display: none; + width: 100%; + height: 0.5px; + margin: 8px 0 10px; + background: color-mix(in srgb, var(--dsw-alias-border-l1) 75%, var(--dsw-alias-border-l2)); + pointer-events: none; +} + +.runningContent { + display: inline-flex; + align-items: center; + min-width: 0; + gap: 6px; +} + +.runningText { + min-width: 0; +} + +.runningNumber { + font-family: var(--ds-font-family-code); + font-variant-numeric: tabular-nums; +} + +.runningIcon { display: inline-flex; flex: none; width: calc(14px + var(--dsh-content-font-delta, 0px)); height: calc(14px + var(--dsh-content-font-delta, 0px)); - color: var(--dsw-alias-label-deep-diving); } .openError { diff --git a/packages/client/ui-chat/src/client/chat/ChatView.tsx b/packages/client/ui-chat/src/client/chat/ChatView.tsx index 3c9650da71..460f98b014 100644 --- a/packages/client/ui-chat/src/client/chat/ChatView.tsx +++ b/packages/client/ui-chat/src/client/chat/ChatView.tsx @@ -18,7 +18,7 @@ import { ChatGroupSeat } from './ChatGroupSeat.tsx' import { chatRenderKey } from './render-entry.ts' import { assertNever } from '@deepseek-ai/dsh-util-values' import { TurnNavigator } from './TurnNavigator.tsx' -import { RunningWhaleTail } from './RunningWhaleTail.tsx' +import { RunningStatus } from './RunningStatus.tsx' import { mergeTurnRailItems } from './turn-rail-items.ts' import { useChatScroll } from './use-chat-scroll.ts' import { fileMediaUrl, resolveWorkspacePath } from '@deepseek-ai/dsh-util-workspace-path' @@ -112,6 +112,13 @@ export function ChatView({ // both the data and its change signal: the array identity moves only when a // Turn enters, leaves, or changes its preview. const turnNavigationItems = useChat(s => s.navigation.items()) + const latestTurnAnchor = turnNavigationItems.at(-1)?.anchorKey + const runningStartTime = useChatNode(latestTurnAnchor ?? '', (node) => { + const location = node?.location + return location?.kind === 'turn' || location?.kind === 'step' + ? location.turn.status === 'open' ? location.turn.start?.time : undefined + : undefined + }) // Host-computed whole-log outline; the merge is view-layer only (the // conversation snapshot never carries projection values). const turnOutline = useProjection('turnOutline') @@ -270,7 +277,7 @@ export function ChatView({ t={t} /> - {running && } + {running && } {/* No pending placeholders: questions (ui-user-questions) and approvals (ApprovalPanel) both take over the composer, so a flow card would double-render the same wait. */} diff --git a/packages/client/ui-chat/src/client/chat/RunningStatus.tsx b/packages/client/ui-chat/src/client/chat/RunningStatus.tsx new file mode 100644 index 0000000000..c6b17921ed --- /dev/null +++ b/packages/client/ui-chat/src/client/chat/RunningStatus.tsx @@ -0,0 +1,45 @@ +/** Running Turn clock isolated from the transcript's render cycle. */ +import { memo, useEffect, useState } from 'react' +import { TextShimmer } from '@deepseek-ai/dsh-client-ui-primitives' +import type { ChatViewSlotProps } from '../contract/slots.ts' +import { formatLiveRunDuration, LIVE_RUN_CLOCK_INTERVAL_MS } from './message-chrome.ts' +import { RunningWhaleTail } from './RunningWhaleTail.tsx' +import a11yCss from './accessibility.module.css' +import css from './ChatView.module.css' + +interface RunningStatusProps { + readonly startTime: number | undefined + 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. + * @returns the blue running indicator; mount only while the Session is running. + */ +export const RunningStatus = memo(function RunningStatus({ startTime, t }: RunningStatusProps) { + const [now, setNow] = useState(Date.now) + useEffect(() => { + if (startTime === undefined) return + setNow(Date.now()) + const timer = setInterval(() => { setNow(Date.now()) }, LIVE_RUN_CLOCK_INTERVAL_MS) + return () => { clearInterval(timer) } + }, [startTime]) + const label = startTime === undefined ? t('chat.deepDiving') : t('chat.deepDivingFor', { + duration: formatLiveRunDuration(Math.max(1000, now - startTime), t), + }) + return ( +
+ {t('chat.deepDiving')} +
+ ) +}) diff --git a/packages/client/ui-chat/src/client/chat/RunningWhaleTail.tsx b/packages/client/ui-chat/src/client/chat/RunningWhaleTail.tsx index 23d4cbb4ad..19ecae8359 100644 --- a/packages/client/ui-chat/src/client/chat/RunningWhaleTail.tsx +++ b/packages/client/ui-chat/src/client/chat/RunningWhaleTail.tsx @@ -52,7 +52,7 @@ export function RunningWhaleTail() { return () => { preference.removeEventListener('change', update) } }, []) return ( -