Merge pull request #6287 from deepseek-harness/turtle/reconnect-timer-1f7094496bf8

fix(ui-chat): pause the running clock while reconnecting
This commit is contained in:
Turtle authored and GitHub committed 2026-10-20 14:20:50 +08:00
commit 37dfd82736
20 files changed
+96 -29

No files matched your search

+2
View File
@@ -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:*",
+6 -3
View File
@@ -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<ChatViewInjected, 'hooks' | 'keyedHooks'> => {
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))
@@ -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 ? [<RunningStatus key="running" startTime={runningStartTime} t={t} />] : [],
...running ? [<RunningStatus key="running" startTime={runningStartTime} paused={disconnected} t={t} />] : [],
...pendingRows,
]
})
@@ -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(''),
})
@@ -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<ChatViewInjected, 'hooks' | 'keyedHooks'>
/** Session-owned node sources shared with the view, plus the flow's connection state. */
export type ChatFlowDataInjected = Pick<ChatViewInjected, 'keyedHooks'> & {
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<ChatNodeOwnerProps,
@@ -24,8 +24,8 @@
en: 75564701ecd3d1bd
zh: 9c042288a15d441d
/chat-conversation-node-rules/whole-turn-folding:
en: f434416f393641fc
zh: 9ef356d6a264cf05
en: 09314600edba4979
zh: da8d599372c7899c
/chat-conversation-node-rules/display-modes:
en: ada5a60d2c46f6a8
zh: f3eaeb5cb4d9b3de
@@ -103,7 +103,7 @@ Clicking Load older anchors the first visible content item below that button in
Paging adds older content above the retained anchor without jumping to the new top. A capped group absorbs the displacement within its scroll range; the outer transcript absorbs the remainder, including when the group first reaches its cap. If the available scroll range is insufficient, compensation stops at the actual limit without adding bottom space. Later content growth keeps the same anchor until a reading gesture or explicit navigation releases it. Typing or clicking within the composer and non-scrolling transcript keys retain the anchor. If the reader scrolls while a page is loading, scrolling takes priority and its settled reading position becomes the new paging anchor.
While the Session runs, Chat shows a Session-level indicator at the running Turn's tail: a whale tail beside shimmering elapsed time. It follows the transcript rows and the echoes of submissions that open a Turn; steering still pending when a submission opens the next Turn is claimed first, so its echoes precede that submission's echo. Echoes of steering that waits for the running Turn's next Step follow the indicator. While the running Step has no output, the indicator sits directly below the input. When the first output row appears, the indicator slides from that position into its slot below the row; the travel equals the row's gap plus one row height, which follows the font size setting, so it starts exactly where it stood. Later output moves it without animation. When the transcript overflows the viewport and follows its bottom, following scrolls keep the indicator in place as output grows, so this slide does not play; admitted steering still moves the indicator past its pending echo, so the steering entrance plays while the page scrolls by the added slot. The steering entrance starts one slot above the indicator's new position rather than where it stood, and its fade-in covers the difference. Before the current Turn start time arrives, the indicator shows status text without a duration. The clock updates in whole seconds, starts at one second, and uses hours from 60 minutes. When the Session stops running, the indicator disappears; the closed Turn process control shows the fixed duration, cancellation, or failure status. Lifecycle changes have a polite announcement; clock ticks do not. The whale is hidden from assistive technology and remains static under reduced motion.
While the Session runs, Chat shows a Session-level indicator at the running Turn's tail: a whale tail beside shimmering elapsed time. It follows the transcript rows and the echoes of submissions that open a Turn; steering still pending when a submission opens the next Turn is claimed first, so its echoes precede that submission's echo. Echoes of steering that waits for the running Turn's next Step follow the indicator. While the running Step has no output, the indicator sits directly below the input. When the first output row appears, the indicator slides from that position into its slot below the row; the travel equals the row's gap plus one row height, which follows the font size setting, so it starts exactly where it stood. Later output moves it without animation. When the transcript overflows the viewport and follows its bottom, following scrolls keep the indicator in place as output grows, so this slide does not play; admitted steering still moves the indicator past its pending echo, so the steering entrance plays while the page scrolls by the added slot. The steering entrance starts one slot above the indicator's new position rather than where it stood, and its fade-in covers the difference. Before the current Turn start time arrives, the indicator shows status text without a duration. The clock updates in whole seconds, starts at one second, and uses hours from 60 minutes. While the Client connection is disconnected or reconnecting, the clock holds its last value; after reconnection it shows the elapsed time since the Turn start again. When the Session stops running, the indicator disappears; the closed Turn process control shows the fixed duration, cancellation, or failure status. Lifecycle changes have a polite announcement; clock ticks do not. The whale is hidden from assistive technology and remains static under reduced motion.
The top duration/status control and trigger title use the group-title font size and follow the font-size setting. Completed duration digits use the code font with tabular numerals; running and completed minutes and seconds have no leading zero. The timed running label ends with a space and “···”; completion shows “Completed in”.
@@ -103,7 +103,7 @@ description: "Chat 过程分组与活动摘要的明细业务规则。"
分页把更早内容加到保留的锚点上方,不主动跳到新内容顶部。限高组先在自己的滚动范围内吸收位移,剩余部分由外层文本滚动补偿,包括组正文刚达到限高的情况。可滚动范围不足时,补偿停在实际边界,不增加底部空白。后续内容撑高沿用同一个锚点,直到阅读手势或显式导航释放它。在 composer 中打字或点击,以及 transcript 内的非滚动按键,保留锚点。分页请求期间若读者滚动,优先服从滚动,停止后以新的阅读位置作为分页锚点。
会话运行期间,Chat 在运行中 Turn 的末尾显示 Session 级运行指示:鲸鱼摆尾旁显示闪烁的已用时间。它位于 transcript 行和开启 Turn 的提交回显之后;某次提交开启下一个 Turn 时仍在等待的 steering 会先被领取,因此其回显排在该提交回显之前。等待运行中 Turn 下一个 Step 的 steering 回显位于运行指示之后。运行中的 Step 尚无输出时,运行指示紧贴在输入下方。第一条输出行出现时,运行指示从原位置滑入该行下方的位置;滑动距离等于该行间距加一行高度(随字号设置变化),因此起点正是它原来的位置。之后的输出不再带动画。transcript 超出视口并跟随底部时,输出增长由跟随滚动吸收,运行指示保持原位,因此这次下滑不播放;被领取的 steering 仍会让运行指示越过其等待中的回显,因此 steering 进场照常播放,同时页面随新增的槽位滚动。steering 进场从运行指示新位置上方一个槽位处开始,而不是从它原来的位置开始,淡入掩盖了这段差距。当前 Turn 的开始时间到达前,运行指示只显示状态文字,不显示时长。时钟按整秒更新,最少显示一秒,满 60 分钟使用小时。会话停止运行后,该指示消失;已结束 Turn 的过程控件显示固定时长、取消或失败状态。生命周期变化有礼貌播报,时钟跳动不播报。鲸鱼对辅助技术隐藏,启用减少动态效果时保持静态。
会话运行期间,Chat 在运行中 Turn 的末尾显示 Session 级运行指示:鲸鱼摆尾旁显示闪烁的已用时间。它位于 transcript 行和开启 Turn 的提交回显之后;某次提交开启下一个 Turn 时仍在等待的 steering 会先被领取,因此其回显排在该提交回显之前。等待运行中 Turn 下一个 Step 的 steering 回显位于运行指示之后。运行中的 Step 尚无输出时,运行指示紧贴在输入下方。第一条输出行出现时,运行指示从原位置滑入该行下方的位置;滑动距离等于该行间距加一行高度(随字号设置变化),因此起点正是它原来的位置。之后的输出不再带动画。transcript 超出视口并跟随底部时,输出增长由跟随滚动吸收,运行指示保持原位,因此这次下滑不播放;被领取的 steering 仍会让运行指示越过其等待中的回显,因此 steering 进场照常播放,同时页面随新增的槽位滚动。steering 进场从运行指示新位置上方一个槽位处开始,而不是从它原来的位置开始,淡入掩盖了这段差距。当前 Turn 的开始时间到达前,运行指示只显示状态文字,不显示时长。时钟按整秒更新,最少显示一秒,满 60 分钟使用小时。Client 连接断开或重新连接期间,时钟停在最后显示的值;恢复连接后重新显示自 Turn 开始以来的用时。会话停止运行后,该指示消失;已结束 Turn 的过程控件显示固定时长、取消或失败状态。生命周期变化有礼貌播报,时钟跳动不播报。鲸鱼对辅助技术隐藏,启用减少动态效果时保持静态。
顶部用时/状态控件与唤醒标题沿用二级汇总标题字号,并跟随字号设置。已完成用时的数字使用代码等宽字体;运行和完成状态的分钟和秒数均不补前导零。含用时的运行文案末尾附加一个空格及“···”,完成后显示“已完成,用时”。
@@ -61,6 +61,7 @@ async function bench(initialSettings?: ChatSettings, withBrowserRegistry = true,
get: (id: string) => id === CHAT_SETTINGS_NAMESPACE ? chatSettings.scope : stubConfigForm().scope,
} as never)
const layout = { closeRightbar: vi.fn(), openRightbar: vi.fn() }
runtime.ctx.provide('connection', { state: createSnapshotStore('connected') } as never)
runtime.ctx.provide('layout', layout as never)
const sidebarRight = {
openResource: vi.fn<(address: string) => void>(),
@@ -466,6 +467,8 @@ describe('Chat inject API', () => {
expect(flow.injected.keyedHooks.chatNode('missing')).toBe(injected.keyedHooks.chatNode('missing'))
expect(flow.injected.keyedHooks.chatNodeProcess('missing')).toBe(injected.keyedHooks.chatNodeProcess('missing'))
expect(flow.injected.keyedHooks.chatNodeBottom('missing')).toBe(injected.keyedHooks.chatNodeBottom('missing'))
expect(flow.injected.hooks.connectionState).toBe(b.runtime.ctx.get('connection')!.state)
expect(flow.injected.hooks.presentation).toBe(injected.hooks.presentation)
expect(injected.fileMentions(owner)).toBeUndefined()
const mentions = { resolve: vi.fn() } as never
@@ -51,6 +51,7 @@ async function bench() {
? chatSettings.scope
: stubConfigForm().scope,
} as never)
runtime.ctx.provide('connection', { state: createSnapshotStore('connected') } as never)
runtime.ctx.provide('layout', { openRightbar: vi.fn(), closeRightbar: vi.fn() } as never)
runtime.ctx.provide('sidebarRight', { openResource: vi.fn(), openTab: vi.fn() } as never)
runtime.ctx.provide('sidebarRightTabs', {
@@ -29,6 +29,7 @@ import type { SessionStatusSnapshot } from '@deepseek-ai/dsh-client-ui-session/c
import type { KeyedSnapshotSelectorHook, SnapshotSelectorHook } from '@deepseek-ai/dsh-client-ui-slots'
import { allowAllAccess, bindSnapshotSelector, makeTranslate } from '@deepseek-ai/dsh-client-test-runtime'
import { createSnapshotStore, type ObservableSnapshot } from '@deepseek-ai/dsh-client-store'
import type { ConnectionState } from '@deepseek-ai/dsh-client-connection/client'
import { EMPTY_CONVERSATION_SNAPSHOT } from '@deepseek-ai/dsh-client-ui-conversation/client'
import { en as commonEn } from '@deepseek-ai/dsh-client-locale/src/locales/en.ts'
import { zh as commonZh } from '@deepseek-ai/dsh-client-locale/src/locales/zh.ts'
@@ -280,18 +281,19 @@ const renderCommandSlot: React.ComponentProps<typeof CommandNodeView>['renderSlo
(_key, _owner, opts) => opts?.fallback ?? null
const renderTurnTailSlot: React.ComponentProps<typeof TurnTailNodeView>['renderSlot'] = () => null
function HarnessChatFlow({ standard, owner, hookContext, renderSlot }: {
function HarnessChatFlow({ standard, owner, hookContext, renderSlot, useConnectionState }: {
readonly standard: HarnessStandardProps
readonly owner: ChatFlowOwnerProps
readonly hookContext: ChatFlowHookContext
readonly renderSlot: ChatFlowSlotProps['renderSlot']
readonly useConnectionState: ChatFlowSlotProps['useConnectionState']
}) {
const hooks = useMemo(() => ({
useGroupAction: CHAT_FLOW_INJECT.hooks.groupAction(standard, hookContext),
useGroupHeaderAction: CHAT_FLOW_INJECT.hooks.groupHeaderAction(standard, hookContext),
}), [standard, hookContext])
const { renderSlot: _rootRenderSlot, __renders: _rootRenders, ...shared } = standard
return <ChatFlow {...shared} {...owner} {...hooks} renderSlot={renderSlot} />
return <ChatFlow {...shared} {...owner} {...hooks} renderSlot={renderSlot} useConnectionState={useConnectionState} />
}
function HarnessChatNode({ standard, owner, hookContext, usePerformanceUsage, toolOwners, fallback, renderAssistantSlot }: {
@@ -456,10 +458,13 @@ function makeHarness(
if ('renderSlot' in owner || 'useStore' in owner || 'actions' in owner) {
throw new Error('Flow owner data must not replace its rendering or store authority')
}
return <HarnessChatFlow standard={props} owner={owner} hookContext={opts.hookContext} renderSlot={renderFlowSlot} />
return <HarnessChatFlow standard={props} owner={owner} hookContext={opts.hookContext} renderSlot={renderFlowSlot}
useConnectionState={useConnectionState} />
}
// SessionProvider seat arrives with the session-scope child declaration;
// ChatView never invokes it (pass-through stub).
const connection = createSnapshotStore<ConnectionState | undefined>('connected')
const useConnectionState = bindSnapshotSelector(connection)
const SessionProviderStub: ChatViewSlotProps['SessionProvider'] = ({ children }) => <>{children}</>
const props: HarnessStandardProps = {
renderFactorySlot,
@@ -537,6 +542,7 @@ function makeHarness(
}
return {
set, setSession: session.set, setChat: chatSource.set, ChatView, props,
setConnection: (state: ConnectionState) => { connection.set(state) },
openFile, openUserFile, loadOlder, loadThrough, openView,
setOutline: (value: unknown) => { outlineValue = value },
chatScroll, forkAt, toolOwners,
@@ -3748,6 +3754,24 @@ describe('ChatView', () => {
expect(vi.getTimerCount()).toBe(0)
}))
it('the running clock freezes while the connection is recovering', withClock(10_000, () => {
const h = makeHarness(
{ nodes: [{ ...userInTurn(1, 'go', 1), time: 2_000 }], turnTimings: new Map([[1, { startTime: 1_000 }]]) },
{ running: true },
)
const view = render(<h.ChatView {...h.props} />)
const content = runningContent(view.container)!
expect(content.textContent).toBe('深度求索中,用时 9秒 ···')
act(() => { h.setConnection('connecting') })
act(() => { vi.advanceTimersByTime(5_000) })
expect(content.textContent).toBe('深度求索中,用时 9秒 ···')
act(() => { h.setConnection('disconnected') })
act(() => { vi.advanceTimersByTime(5_000) })
expect(content.textContent).toBe('深度求索中,用时 9秒 ···')
act(() => { h.setConnection('connected') })
expect(content.textContent).toBe('深度求索中,用时 19秒 ···')
}))
it('the running clock reads hours once the turn passes an hour', withClock(3_600_000, () => {
const startTime = 1_000
const trigger = { ...userInTurn(1, 'go', 1), time: startTime + 1 }
@@ -52,6 +52,7 @@ async function bench() {
developerTools: { enabled: createSnapshotStore(true) },
get: (namespace: string) => namespace === CHAT_SETTINGS_NAMESPACE ? chatSettings.scope : stubConfigForm().scope,
} as never)
runtime.ctx.provide('connection', { state: createSnapshotStore('connected') } as never)
runtime.ctx.provide('layout', { openRightbar: () => {}, closeRightbar: () => {} } as never)
runtime.ctx.provide('sidebarRight', { openResource: () => {}, openTab: () => {} } as never)
runtime.ctx.provide('sidebarRightTabs', {
@@ -13,11 +13,13 @@ beforeEach(() => { vi.useFakeTimers(); vi.setSystemTime(5_000) })
afterEach(() => { cleanup(); vi.useRealTimers(); vi.restoreAllMocks(); vi.unstubAllGlobals() })
function statusHarness(startTime?: number) {
const view = render(<RunningStatus startTime={startTime} t={t} />)
const view = render(<RunningStatus startTime={startTime} paused={false} t={t} />)
return {
...view,
content: () => view.container.querySelector('[data-chat-running] > :last-child'),
set: (nextStartTime?: number) => { view.rerender(<RunningStatus startTime={nextStartTime} t={t} />) },
set: (nextStartTime?: number, paused = false) => {
view.rerender(<RunningStatus startTime={nextStartTime} paused={paused} t={t} />)
},
}
}
@@ -66,4 +68,15 @@ describe('RunningStatus', () => {
expect(view.content()?.textContent).not.toContain('-')
})
it('freezes the duration while paused and resumes from the Turn start', () => {
const view = statusHarness(1_000)
expect(view.content()?.textContent).toBe('深度求索中,用时 4秒 ···')
view.set(1_000, true)
expect(vi.getTimerCount()).toBe(0)
act(() => { vi.advanceTimersByTime(5_000) })
expect(view.content()?.textContent).toBe('深度求索中,用时 4秒 ···')
view.set(1_000)
expect(view.content()?.textContent).toBe('深度求索中,用时 9秒 ···')
expect(vi.getTimerCount()).toBe(1)
})
})
@@ -69,6 +69,9 @@
{
"path": "../../subagent/subagent"
},
{
"path": "../connection/tsconfig.client.json"
},
{
"path": "../locale/tsconfig.client.json"
},
@@ -94,6 +94,7 @@ async function bench() {
developerTools: { enabled: createSnapshotStore(true) },
get: formFor,
} as never)
runtime.ctx.provide('connection', { state: createSnapshotStore('connected') } as never)
runtime.ctx.provide('layout', { openRightbar: () => {}, closeRightbar: () => {} } as never)
runtime.ctx.provide('sidebarRight', { openResource: () => {}, openTab: () => {} } as never)
runtime.ctx.provide('sidebarRightTabs', {
@@ -89,6 +89,7 @@ async function bench(nodes: ToolResultNode[]) {
},
})
runtime.ctx.provide('configForms', { developerTools: { enabled: createSnapshotStore(true) }, get: () => stubConfigForm().scope } as never)
runtime.ctx.provide('connection', { state: createSnapshotStore('connected') } as never)
runtime.ctx.provide('layout', { openDetails: vi.fn(), closeDetails: vi.fn() })
runtime.ctx.provide('sidebarRight', { openResource: vi.fn() } as never)
runtime.ctx.provide('uiWorkspace', {
@@ -108,6 +108,7 @@ async function bench(snapshot: ChatSnapshot) {
const conversation = createSnapshotStore(EMPTY_CONVERSATION_SNAPSHOT)
const events = new ConversationEventRegistry(ctx)
const views = new ConversationViewRegistry(ctx)
ctx.provide('connection', { state: createSnapshotStore('connected') } as never)
ctx.provide('configForms', { developerTools: { enabled: createSnapshotStore(true) }, get: () => stubConfigForm().scope } as never)
ctx.provide('uiConversation', {
events,
@@ -80,6 +80,7 @@ async function bench(nodes: ToolResultNode[]) {
runtime.remote.provideNamespaces({ session: { openWorkspacePath } })
runtime.ctx.provide('configForms', { developerTools: { enabled: createSnapshotStore(true) }, get: () => stubConfigForm().scope } as never)
const layout = { openDetails: vi.fn(), closeDetails: vi.fn() }
runtime.ctx.provide('connection', { state: createSnapshotStore('connected') } as never)
runtime.ctx.provide('layout', layout)
const sidebarRight = { openResource: vi.fn<(address: string) => void>() }
runtime.ctx.provide('sidebarRight', sidebarRight as never)
@@ -300,6 +301,7 @@ describe('registrant declaration injection', () => {
},
})
runtime.ctx.provide('configForms', { developerTools: { enabled: createSnapshotStore(true) }, get: () => stubConfigForm().scope } as never)
runtime.ctx.provide('connection', { state: createSnapshotStore('connected') } as never)
runtime.ctx.provide('layout', { openDetails: vi.fn(), closeDetails: vi.fn() })
runtime.ctx.provide('sidebarRight', { openResource: vi.fn() } as never)
runtime.ctx.provide('uiWorkspace', {
@@ -182,7 +182,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
],
replaceRisk: 'none',
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'conversation.chat.assistant-actions\', () => ctx.slots.register(\n { name: \'conversation.chat.assistant-actions\', id: \'my-entry\', order: 100, label: \'My entry\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
source: 'packages/client/ui-chat/src/client/contract/slots.ts:441',
source: 'packages/client/ui-chat/src/client/contract/slots.ts:447',
},
{
key: 'conversation.chat.commandview',
@@ -233,7 +233,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
occupants: [],
replaceRisk: 'none',
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'conversation.chat.commandview\', () => ctx.slots.register(\n { name: \'conversation.chat.commandview\', key: \'<one key the owner dispatches>\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
source: 'packages/client/ui-chat/src/client/contract/slots.ts:423',
source: 'packages/client/ui-chat/src/client/contract/slots.ts:429',
},
{
key: 'conversation.chat.flow',
@@ -279,7 +279,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
],
replaceRisk: 'shadows-shipped-ui',
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'conversation.chat.flow\', () => ctx.slots.register(\n { name: \'conversation.chat.flow\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
source: 'packages/client/ui-chat/src/client/contract/slots.ts:390',
source: 'packages/client/ui-chat/src/client/contract/slots.ts:396',
},
{
key: 'conversation.chat.message.images',
@@ -325,7 +325,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
],
replaceRisk: 'shadows-shipped-ui',
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'conversation.chat.message.images\', () => ctx.slots.register(\n { name: \'conversation.chat.message.images\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
source: 'packages/client/ui-chat/src/client/contract/slots.ts:415',
source: 'packages/client/ui-chat/src/client/contract/slots.ts:421',
},
{
key: 'conversation.chat.node',
@@ -398,7 +398,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
],
replaceRisk: 'shadows-shipped-ui',
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'conversation.chat.node\', () => ctx.slots.register(\n { name: \'conversation.chat.node\', key: \'<one key the owner dispatches>\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
source: 'packages/client/ui-chat/src/client/contract/slots.ts:402',
source: 'packages/client/ui-chat/src/client/contract/slots.ts:408',
},
{
key: 'conversation.chat.reasoning.body',
@@ -441,7 +441,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
],
replaceRisk: 'shadows-shipped-ui',
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'conversation.chat.reasoning.body\', () => ctx.slots.register(\n { name: \'conversation.chat.reasoning.body\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
source: 'packages/client/ui-chat/src/client/contract/slots.ts:429',
source: 'packages/client/ui-chat/src/client/contract/slots.ts:435',
},
{
key: 'conversation.chat.turnTail',
@@ -506,7 +506,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
],
replaceRisk: 'none',
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'conversation.chat.turnTail\', () => ctx.slots.register(\n { name: \'conversation.chat.turnTail\', id: \'my-entry\', order: 100, label: \'My entry\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
source: 'packages/client/ui-chat/src/client/contract/slots.ts:435',
source: 'packages/client/ui-chat/src/client/contract/slots.ts:441',
},
{
key: 'conversation.chat.user.images',
@@ -552,7 +552,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
],
replaceRisk: 'shadows-shipped-ui',
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'conversation.chat.user.images\', () => ctx.slots.register(\n { name: \'conversation.chat.user.images\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
source: 'packages/client/ui-chat/src/client/contract/slots.ts:417',
source: 'packages/client/ui-chat/src/client/contract/slots.ts:423',
},
{
key: 'conversation.composer',
@@ -3447,7 +3447,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
],
replaceRisk: 'none',
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'shell.quota-notice\', () => ctx.slots.register(\n { name: \'shell.quota-notice\', select: owner => null },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
source: 'packages/client/ui-chat/src/client/contract/slots.ts:449',
source: 'packages/client/ui-chat/src/client/contract/slots.ts:455',
},
{
key: 'sidebar',
+3
View File
@@ -3711,6 +3711,9 @@ importers:
'@deepseek-ai/dsh-brand':
specifier: workspace:*
version: link:../../util/brand
'@deepseek-ai/dsh-client-connection':
specifier: workspace:*
version: link:../connection
'@deepseek-ai/dsh-client-locale':
specifier: workspace:*
version: link:../locale