Merge remote-tracking branch 'origin/master' into xtr/agent-loop-message-machine

# Conflicts:
#	docs/cordis-catalog/services.md
#	examples/acp-agent/tests/snapshots/code-mode-workspace-context/session.jsonl
#	packages/core/tools/README.i18n.yaml
#	packages/core/tools/src/code-mode.ts
This commit is contained in:
_Kerman
2026-07-26 22:17:47 +08:00
77 changed files with 3404 additions and 1456 deletions

View File

@@ -45,6 +45,18 @@
outline-offset: 1px;
}
/* run_code sub-dispatch rows: indented under the parent row, left-edged so
the code turn reads as one unit; each nested row is itself a .callRow
(same components, same selection outline as top-level rows). */
.subCalls {
display: flex;
flex-direction: column;
gap: 4px;
margin: 4px 0 2px 22px;
padding-left: 8px;
border-left: 1px solid var(--dsw-alias-border-l2);
}
.hint {
color: var(--dsw-alias-label-tertiary);
font-size: 12px;

View File

@@ -20,7 +20,7 @@ import {
memo, useLayoutEffect, useMemo, useRef, useState, type ReactNode,
} from 'react'
import type {
ConversationNode, ConversationSnapshot, RunningToolCall, ToolResultNode,
CodeSubCall, ConversationNode, ConversationSnapshot, RunningToolCall, ToolResultNode,
} from '@deepseek-ai/dsh-client-runtime/client'
import type { SnapshotSelectorHook } from '@deepseek-ai/dsh-client-ui-slots'
import { IconChevronDownOutline14 } from '@deepseek-ai/dsh-client-ui-primitives'
@@ -45,10 +45,39 @@ type RenderToolRow = ChatViewSlotProps['renderSlot']
* chat view narrows once to the runtime snapshot the binding actually feeds. */
type UseConversation = SnapshotSelectorHook<ConversationSnapshot>
/** One `run_code` sub-dispatch row: the identical keyed-slot dispatch as a
* top-level call (same registrations, same fallback), nested by the parent.
* A started-but-unsettled sub-call arrives as the RunningToolCall shape and
* renders the running state exactly as a native in-flight row. */
const SubCallRow = memo(function SubCallRow({ renderSlot, node, onOpenDetails, selected }: {
renderSlot: RenderToolRow
node: CodeSubCall
onOpenDetails: OpenDetails
selected: boolean
}) {
const settled = 'kind' in node
const toolName = settled ? node.call?.name ?? '' : node.name
const seq = settled ? node.seq : node.time
const owner = useMemo(() => ({
callId: node.callId, toolName, block: node,
openDetails: () => { onOpenDetails({ turnSeq: seq, callId: node.callId, toolName }) },
}), [node, toolName, seq, onOpenDetails])
return (
<div className={css.callRow} data-selected={selected || undefined}>
{renderSlot('conversation.chat.toolview', owner, {
entryKey: toolName,
fallback: <GenericToolCard {...owner} />,
})}
</div>
)
})
/** One tool call row (result or running): dispatches through the keyed
* toolview slot with the owner payload; unregistered tools fall back to
* GenericToolCard at this render site. */
const CallRow = memo(function CallRow({ renderSlot, callId, toolName, block, seq, onOpenDetails, selected }: {
* GenericToolCard at this render site. A `run_code` call additionally
* renders its logged sub-dispatches as always-visible indented rows —
* each one the same keyed-slot dispatch as a native top-level call. */
const CallRow = memo(function CallRow({ renderSlot, callId, toolName, block, seq, onOpenDetails, selected, subCalls, selectedCallId }: {
renderSlot: RenderToolRow
callId: string
toolName: string
@@ -57,6 +86,10 @@ const CallRow = memo(function CallRow({ renderSlot, callId, toolName, block, seq
seq: number
onOpenDetails: OpenDetails
selected: boolean
/** `run_code` sub-dispatches in dispatch order (reference-stable per parent; running entries settle in place); undefined for ordinary calls. */
subCalls?: readonly CodeSubCall[] | undefined
/** The store's selected callId, matched against sub-rows (undefined when no sub-row here is selected). */
selectedCallId?: string | undefined
}) {
const owner = useMemo(() => ({
callId, toolName, block,
@@ -68,17 +101,32 @@ const CallRow = memo(function CallRow({ renderSlot, callId, toolName, block, seq
entryKey: toolName,
fallback: <GenericToolCard {...owner} />,
})}
{subCalls !== undefined && subCalls.length > 0 && (
<div className={css.subCalls} data-subcalls>
{subCalls.map((node) => (
<SubCallRow
key={node.callId}
renderSlot={renderSlot}
node={node}
onOpenDetails={onOpenDetails}
selected={node.callId === selectedCallId}
/>
))}
</div>
)}
</div>
)
})
/** Consecutive tool results as one step-run group (figma VERTICAL gap10). */
const ToolGroup = memo(function ToolGroup({ renderSlot, results, onOpenDetails, selectedCallId }: {
const ToolGroup = memo(function ToolGroup({ renderSlot, results, onOpenDetails, selectedCallId, codeDispatches }: {
renderSlot: RenderToolRow
results: readonly ToolResultNode[]
onOpenDetails: OpenDetails
/** Only set when the selected call lives in THIS group (memo economy). */
/** Only set when the selected call lives in THIS group, top-level or nested (memo economy). */
selectedCallId: string | undefined
/** Sub-dispatch index off the snapshot (map reference is chunk-storm stable). */
codeDispatches: ReadonlyMap<string, readonly CodeSubCall[]>
}) {
return (
<div className={css.toolGroup}>
@@ -92,6 +140,8 @@ const ToolGroup = memo(function ToolGroup({ renderSlot, results, onOpenDetails,
seq={node.seq}
onOpenDetails={onOpenDetails}
selected={node.callId === selectedCallId}
subCalls={codeDispatches.get(node.callId)}
selectedCallId={selectedCallId}
/>
))}
</div>
@@ -116,6 +166,7 @@ function StreamingTail({ useSession, onGrow }: {
export function ChatView({ useSession, useStore, renderSlot, openDetails, loadOlder }: ChatViewSlotProps) {
const nodes = useSession((s) => s.nodes)
const runningCalls = useSession((s) => s.runningCalls)
const codeDispatches = useSession((s) => s.codeDispatches)
const pending = useSession((s) => s.pending)
const openState = useSession((s) => s.openState)
const openErrorMessage = useSession((s) => s.openError === null ? null : `${s.openError.message}${s.openError.code}`)
@@ -203,7 +254,8 @@ export function ChatView({ useSession, useStore, renderSlot, openDetails, loadOl
const renderItem = (item: ChatFlowItem): ReactNode => {
if (item.kind === 'tool-group') {
const inGroup = selectedCallId !== undefined
&& item.results.some((r) => r.callId === selectedCallId)
&& item.results.some((r) => r.callId === selectedCallId
|| codeDispatches.get(r.callId)?.some((sub) => sub.callId === selectedCallId) === true)
return (
<ToolGroup
key={item.key}
@@ -211,6 +263,7 @@ export function ChatView({ useSession, useStore, renderSlot, openDetails, loadOl
results={item.results}
onOpenDetails={openDetails}
selectedCallId={inGroup ? selectedCallId : undefined}
codeDispatches={codeDispatches}
/>
)
}
@@ -250,6 +303,8 @@ export function ChatView({ useSession, useStore, renderSlot, openDetails, loadOl
seq={call.turn}
onOpenDetails={openDetails}
selected={call.callId === selectedCallId}
subCalls={codeDispatches.get(call.callId)}
selectedCallId={selectedCallId}
/>
))}
</div>

View File

@@ -6,7 +6,7 @@
import type { ReactNode } from 'react'
import {
IconApiOutline14, IconBrowseOutline16, IconEditOutline16, IconSearchOutline16, IconThinkOutline14,
IconApiOutline14, IconBrowseOutline16, IconCodeOutline16, IconEditOutline16, IconSearchOutline16, IconThinkOutline14,
} from '@deepseek-ai/dsh-client-ui-primitives'
import type { ToolRowOwnerProps } from '../contract/slots.ts'
import { toolRowModel, type ToolRowVariant } from '../contract/tool-call-model.ts'
@@ -21,6 +21,7 @@ const VARIANT_ICONS: Record<ToolRowVariant, ReactNode> = {
bash: <IconApiOutline14 size={16} />,
write: <IconEditOutline16 />,
edit: <IconEditOutline16 />,
code: <IconCodeOutline16 />,
others: <IconSparkle16 />,
}

View File

@@ -86,3 +86,15 @@ button.leading {
word-break: break-word;
color: var(--dsw-alias-label-tertiary);
}
/* The code variant's expanded body is the run_code program: monospace on the
markdown code-block fill so the program reads as code, not prose. */
.root[data-variant='code'] .body {
font-family: var(--ds-font-family-code);
font-size: 13px;
line-height: 20px;
padding: 6px 8px;
margin-left: 22px;
border-radius: 6px;
background: var(--dsw-alias-markdown-code-block);
}

View File

@@ -13,8 +13,8 @@ export type { ToolCallBlock } from '@deepseek-ai/dsh-client-runtime/client'
/** The frozen slice the chat view hands to toolview components as `block`
* (both members are cache-stable references off ConversationSnapshot). */
/** The seven row variants (think is fed by reasoning blocks, not tool calls). */
export type ToolRowVariant = 'think' | 'search' | 'read' | 'bash' | 'write' | 'edit' | 'others'
/** The eight row variants (think is fed by reasoning blocks, not tool calls). */
export type ToolRowVariant = 'think' | 'search' | 'read' | 'bash' | 'write' | 'edit' | 'code' | 'others'
/** Row state semantic; colors self-supplied via StateDot (design gives none). */
export type ToolRowState = 'running' | 'ok' | 'error' | 'stopped'
@@ -22,7 +22,7 @@ export type ToolRowState = 'running' | 'ok' | 'error' | 'stopped'
/** Figma row titles per variant (design literals, not translatable copy). */
export const VARIANT_TITLES: Record<ToolRowVariant, string> = {
think: 'Think', search: 'Search', read: 'Read', bash: 'Bash',
write: 'Write', edit: 'Edit', others: 'Tool call',
write: 'Write', edit: 'Edit', code: 'Code', others: 'Tool call',
}
/** Known tool name -> variant. */
@@ -35,6 +35,7 @@ const TOOL_VARIANTS: Record<string, ToolRowVariant> = {
glob: 'search',
write: 'write',
edit: 'edit',
run_code: 'code',
}
/**
@@ -86,6 +87,7 @@ const SUMMARY_KEYS: Record<ToolRowVariant, readonly string[]> = {
think: [],
write: ['path', 'file_path'],
edit: ['path', 'file_path'],
code: ['description'],
others: [],
}
@@ -101,10 +103,17 @@ function deriveSummary(variant: ToolRowVariant, argsRaw: string): string {
return firstLine(argsRaw)
}
function deriveBody(argsRaw: string): string | null {
function deriveBody(variant: ToolRowVariant, argsRaw: string): string | null {
if (argsRaw === '') return null
const parsed = parseArgs(argsRaw)
return parsed === undefined ? argsRaw : JSON.stringify(parsed, null, 2)
if (parsed === undefined) return argsRaw
// The code row's expanded body IS the program (monospace via the row's
// variant styling), not the args JSON envelope around it.
if (variant === 'code' && typeof parsed === 'object' && parsed !== null) {
const code = (parsed as Record<string, unknown>).code
if (typeof code === 'string' && code !== '') return code
}
return JSON.stringify(parsed, null, 2)
}
/**
@@ -128,7 +137,7 @@ export function toolRowModel(toolName: string, block: ToolCallBlock): ToolRowMod
variant,
title: VARIANT_TITLES[variant],
summary,
body: deriveBody(argsRaw),
body: deriveBody(variant, argsRaw),
state,
}
}

View File

@@ -31,6 +31,18 @@ function materialFor(s: ConversationSnapshot, callId: string): CallMaterial | nu
if (open !== undefined) {
return { name: open.name, argsRaw: open.argsRaw, result: null, running: true }
}
// run_code sub-dispatches: the native call-block shapes, so a selected
// sub-row resolves through the same material as a native call — the
// settled ToolResultNode form, or the RunningToolCall form mid-flight.
for (const subs of s.codeDispatches.values()) {
for (const sub of subs) {
if (sub.callId !== callId) continue
if ('kind' in sub) {
return { name: sub.call?.name ?? callId, argsRaw: sub.call?.argsRaw ?? null, result: sub, running: false }
}
return { name: sub.name, argsRaw: sub.argsRaw, result: null, running: true }
}
}
return null
}