feat(tool): show content preparation progress for file mutations
This commit is contained in:
1 parent
fb8bb16345
commit
b660f72d4e
7 files changed
+77
-10
No files matched your search
@@ -85,6 +85,7 @@ export const zh = {
|
||||
'tool.title.createGoal': '创建目标',
|
||||
'tool.title.getGoal': '查看目标',
|
||||
'tool.title.updateGoal': '更新目标',
|
||||
'tool.preparing.content': '正在准备内容 {kilobytes}KB',
|
||||
'tool.title.createSchedule': '创建定时任务',
|
||||
'tool.title.listSchedules': '查看定时任务',
|
||||
'tool.title.deleteSchedule': '删除定时任务',
|
||||
@@ -438,6 +439,7 @@ export const en = {
|
||||
'tool.title.createGoal': 'Create goal',
|
||||
'tool.title.getGoal': 'View goal',
|
||||
'tool.title.updateGoal': 'Update goal',
|
||||
'tool.preparing.content': 'Preparing content {kilobytes}KB',
|
||||
'tool.title.createSchedule': 'Create reminder',
|
||||
'tool.title.listSchedules': 'List reminders',
|
||||
'tool.title.deleteSchedule': 'Delete reminder',
|
||||
|
||||
@@ -7,6 +7,7 @@ import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
|
||||
import type {} from '@deepseek-ai/dsh-client-ui-renderer/client'
|
||||
import type {} from '@deepseek-ai/dsh-client-ui-session/client'
|
||||
import { ToolCallTree } from './tool/ToolCallTree.tsx'
|
||||
import { bindToolCallArgumentsPartial } from './tool/tool-call-arguments-partial.ts'
|
||||
import { CONVERSATION_NS as NS } from './locale.ts'
|
||||
import { askQuestionToolview } from './tool/toolviews/ask-question-row.tsx'
|
||||
import { bashToolviewSample } from './tool/toolviews/bash-sample.tsx'
|
||||
@@ -36,7 +37,10 @@ export function apply(ctx: ClientContext): void {
|
||||
key: 'tool-call',
|
||||
locale: NS,
|
||||
children: {
|
||||
'tool.call.toolview': { kind: 'keyed', scope: 'session' },
|
||||
'tool.call.toolview': {
|
||||
kind: 'keyed', scope: 'session',
|
||||
inject: { hooks: { toolCallArgumentsPartial: bindToolCallArgumentsPartial } },
|
||||
},
|
||||
},
|
||||
inject: toolInject,
|
||||
}, ToolCallTree))
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
/** Tool UI slot declarations and their composed component props. */
|
||||
import type {
|
||||
HostObservable, InjectFace, PropsLocale, PropsRenderSlots, PropsRuntime,
|
||||
HostObservable, InjectFace, PropsLocale, PropsRenderSlots, PropsRuntime, SlotHookFactory,
|
||||
} from '@deepseek-ai/dsh-client-ui-slots'
|
||||
import type { RemoteHostFacts } from '@deepseek-ai/dsh-api-remotes/client'
|
||||
import type {
|
||||
OpenFileOptions, PreparingToolCall, StartedToolCall,
|
||||
AssistantChatData, OpenFileOptions, PreparingToolCall, StartedToolCall,
|
||||
ToolResultNode, UseDisclosure,
|
||||
} from '@deepseek-ai/dsh-client-ui-chat/client'
|
||||
import type { MessageImageLoader, MessageImageSource } from '@deepseek-ai/dsh-client-ui-conversation/client'
|
||||
@@ -21,7 +21,13 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
|
||||
* generic row. The owner supplies the call identity and frozen running
|
||||
* or settled node through explicit phase props; preparing inputs have no arguments.
|
||||
*/
|
||||
'tool.call.toolview': { kind: 'keyed'; scope: 'session'; owner: ToolCallOwnerProps }
|
||||
'tool.call.toolview': {
|
||||
kind: 'keyed'
|
||||
scope: 'session'
|
||||
owner: ToolCallOwnerProps
|
||||
hookContext: ToolCallHookContext
|
||||
inject: ToolCallInjected
|
||||
}
|
||||
/**
|
||||
* Durable images of a settled image-bearing Tool call, rendered through
|
||||
* the attachment presentation plugin. The Tool layer never imports an
|
||||
@@ -39,6 +45,23 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
|
||||
}
|
||||
}
|
||||
|
||||
/** Subscribe to this preparing call's raw argument prefix; other phases return an empty string. */
|
||||
export type UseToolCallArgumentsPartial = () => string
|
||||
|
||||
/** Call-local sources supplied by the Tool tree to the slot's Hook binding. */
|
||||
export interface ToolCallHookContext {
|
||||
readonly callId: string
|
||||
/** This call's Step source, present only while preparing. */
|
||||
readonly assistant: HostObservable<Readonly<AssistantChatData> | undefined> | undefined
|
||||
}
|
||||
|
||||
/** Framework-bound subscriptions available to atomic Tool views on demand. */
|
||||
export interface ToolCallInjected {
|
||||
hooks: {
|
||||
toolCallArgumentsPartial: SlotHookFactory<'tool.call.toolview', UseToolCallArgumentsPartial>
|
||||
}
|
||||
}
|
||||
|
||||
/** Owner currency of the Tool image gallery slot: references plus the loader. */
|
||||
export interface ToolImagesOwnerProps {
|
||||
/** Durable references or submission-echo previews in result order. */
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
export { apply, inject } from './apply.ts'
|
||||
export type {
|
||||
StartedToolCallViewProps, ToolCallCommonProps, ToolCallOwnerProps, ToolCallPhaseProps, ToolCallViewProps,
|
||||
ToolHostInfoInjected, ToolTreeProps,
|
||||
ToolCallHookContext, ToolCallInjected, ToolHostInfoInjected, ToolTreeProps, UseToolCallArgumentsPartial,
|
||||
} from './contract/slots.ts'
|
||||
@@ -1,7 +1,7 @@
|
||||
/** Root/subcall Tool composition with one keyed atomic dispatch path. */
|
||||
import { memo, useMemo, type ReactNode } from 'react'
|
||||
import type { ToolCallBlock } from '@deepseek-ai/dsh-client-ui-chat/client'
|
||||
import type { ToolCallOwnerProps, ToolCallPhaseProps, ToolTreeProps } from '../contract/slots.ts'
|
||||
import type { ToolCallHookContext, ToolCallOwnerProps, ToolCallPhaseProps, ToolTreeProps } from '../contract/slots.ts'
|
||||
import { toolRowModel } from './models/tool-call-model.ts'
|
||||
import { GenericToolCard } from './toolviews/GenericToolCard.tsx'
|
||||
import css from './ToolCallTree.module.css'
|
||||
@@ -18,14 +18,19 @@ function callName(call: ToolCallPhaseProps): string {
|
||||
|
||||
/** One atomic call dispatched through the Tool-owned keyed slot. */
|
||||
const ToolCall = memo(function ToolCall({
|
||||
renderSlot, callId, toolName, call, openFile, cwd, home, inspectCall, loadImage, useDisclosure, t, children,
|
||||
renderSlot, callId, toolName, call, assistant, openFile, cwd, home, inspectCall, loadImage, useDisclosure, t, children,
|
||||
}: Pick<ToolTreeProps, 'renderSlot' | 'openFile' | 'cwd' | 'inspectCall' | 'loadImage' | 'useDisclosure' | 't'> & {
|
||||
callId: string
|
||||
toolName: string
|
||||
call: ToolCallPhaseProps
|
||||
assistant: ToolCallHookContext['assistant']
|
||||
home?: string | undefined
|
||||
children?: ReactNode
|
||||
}) {
|
||||
const preparing = call.phase === 'preparing'
|
||||
const hookContext = useMemo<ToolCallHookContext>(() => ({
|
||||
callId, assistant: preparing ? assistant : undefined,
|
||||
}), [assistant, callId, preparing])
|
||||
const owner: ToolCallOwnerProps = useMemo(() => ({
|
||||
callId,
|
||||
toolName,
|
||||
@@ -51,6 +56,7 @@ const ToolCall = memo(function ToolCall({
|
||||
? <GenericToolCard {...owner} t={t} />
|
||||
: renderSlot('tool.call.toolview', owner, {
|
||||
entryKey: toolName,
|
||||
hookContext,
|
||||
fallback: <GenericToolCard {...owner} t={t} />,
|
||||
})}
|
||||
{children}
|
||||
@@ -59,9 +65,10 @@ const ToolCall = memo(function ToolCall({
|
||||
})
|
||||
|
||||
const ToolCallBranch = memo(function ToolCallBranch({
|
||||
renderSlot, block, cwd, home, openFile, inspectCall, loadImage, useDisclosure, t,
|
||||
renderSlot, block, assistant, cwd, home, openFile, inspectCall, loadImage, useDisclosure, t,
|
||||
}: Pick<ToolTreeProps, 'renderSlot' | 'cwd' | 'openFile' | 'inspectCall' | 'loadImage' | 'useDisclosure' | 't'> & {
|
||||
block: ToolCallBlock
|
||||
assistant: ToolCallHookContext['assistant']
|
||||
home?: string | undefined
|
||||
}) {
|
||||
const call = useMemo(() => toolCallPhase(block), [block])
|
||||
@@ -71,6 +78,7 @@ const ToolCallBranch = memo(function ToolCallBranch({
|
||||
callId={call.block.callId}
|
||||
toolName={callName(call)}
|
||||
call={call}
|
||||
assistant={assistant}
|
||||
openFile={openFile}
|
||||
cwd={cwd}
|
||||
home={home}
|
||||
@@ -86,6 +94,7 @@ const ToolCallBranch = memo(function ToolCallBranch({
|
||||
key={child.callId}
|
||||
renderSlot={renderSlot}
|
||||
block={child}
|
||||
assistant={assistant}
|
||||
cwd={cwd}
|
||||
home={home}
|
||||
openFile={openFile}
|
||||
@@ -111,10 +120,12 @@ export function ToolCallTree({
|
||||
renderSlot, node, cwd, openFile, inspectCall, loadImage, useDisclosure, useHostInfo, t,
|
||||
}: ToolTreeProps) {
|
||||
const home = useHostInfo(info => info.home)
|
||||
const assistant = node.location.kind === 'step' ? node.location.step.data.source('assistant-step') : undefined
|
||||
return (
|
||||
<ToolCallBranch
|
||||
renderSlot={renderSlot}
|
||||
block={node.data.root}
|
||||
assistant={assistant}
|
||||
cwd={cwd}
|
||||
home={home}
|
||||
openFile={openFile}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
/** Call-local argument subscription bound by the atomic Tool slot. */
|
||||
import { useSyncExternalStore } from 'react'
|
||||
import type { ToolCallInjected } from '../contract/slots.ts'
|
||||
|
||||
const subscribeEmpty = (): (() => void) => () => {}
|
||||
|
||||
/**
|
||||
* Bind one call without subscribing until its component invokes the Hook.
|
||||
* @param _standard - framework-provided scope props.
|
||||
* @param context - the preparing call's Step source and identity.
|
||||
* @returns a Hook that reads only this call's raw argument prefix.
|
||||
*/
|
||||
export const bindToolCallArgumentsPartial: ToolCallInjected['hooks']['toolCallArgumentsPartial'] = (
|
||||
_standard, { assistant, callId },
|
||||
) => {
|
||||
const getSnapshot = (): string => {
|
||||
const block = assistant?.getSnapshot()?.blocks.find(candidate => candidate.kind === 'tool-call' && candidate.callId === callId)
|
||||
return block?.kind === 'tool-call' ? block.argsRaw : ''
|
||||
}
|
||||
return function useToolCallArgumentsPartial() {
|
||||
return useSyncExternalStore(assistant?.subscribe ?? subscribeEmpty, getSnapshot)
|
||||
}
|
||||
}
|
||||
@@ -12,9 +12,13 @@ type FileMutationRowProps = ToolCallViewProps & PropsLocale<'conversation'>
|
||||
/**
|
||||
* Lets users expand an applied file diff and open the reported path.
|
||||
*/
|
||||
export function FileMutationRow({ toolName, block, cwd, home, openFile, inspect, useDisclosure, t }: FileMutationRowProps) {
|
||||
export function FileMutationRow({ toolName, block, phase, cwd, home, openFile, inspect, useDisclosure, useToolCallArgumentsPartial, t }: FileMutationRowProps) {
|
||||
const raw = useToolCallArgumentsPartial()
|
||||
const model = toolRowModel(toolName, block, cwd, home)
|
||||
const diff = diffCardModel(block)
|
||||
const summary = phase === 'preparing'
|
||||
? t('tool.preparing.content', { kilobytes: Math.round(raw.length / 1024) })
|
||||
: model.summary
|
||||
return (
|
||||
<ToolRow
|
||||
useDisclosure={useDisclosure}
|
||||
@@ -23,7 +27,7 @@ export function FileMutationRow({ toolName, block, cwd, home, openFile, inspect,
|
||||
toolName={toolName}
|
||||
icon={<IconEditOutlineRegular size={14} />}
|
||||
title={t(model.titleKey)}
|
||||
summary={model.summary}
|
||||
summary={summary}
|
||||
output={model.output}
|
||||
errorSummary={model.errorSummary}
|
||||
diff={diff}
|
||||
|
||||
Reference in new issue
Block a user