mirror of
https://github.com/stablyai/orca.git
synced 2026-10-07 00:02:29 +00:00
feat(native-chat): read a tool batch as a group
A run of several tool calls collapsed to one joined string: names and arguments run together, separated by a middle dot that also occurs inside `browser.open` and `tools/read`, with the overflow cut mid-token. Opened, the member rows sat flush with the header and with the message content around them, so the batch had no visible end. Two presentation changes, no new derivation: - Each member gets its own bounded pill in the collapsed header, carrying its own category glyph, so the boundary between calls is a shape rather than a character. Pills wrap instead of truncating, and members past the summary cap are counted in `+N more` rather than dropped silently. - Opened members are indented under the header, which is what marks where the run ends. `toolRunSummaryMembers` keeps the run's leading calls apart instead of pre-joining them; `summarizeToolRun` now derives its string from it, so mobile's header is byte-identical and the two cannot disagree about which calls speak for a run. Two existing behaviours are pinned by test rather than changed, both being naming decisions rather than layout ones: the header still prints the raw `mcp__linear__list_issues` while the row beneath prints the split name, and a call carrying only a `url` still falls through to a JSON preview clipped at 28 characters.
This commit is contained in:
@@ -200,7 +200,7 @@ describe('NativeChatMessageList assistant messages', () => {
|
||||
/>
|
||||
)
|
||||
|
||||
const settledTool = screen.getByText('shell pnpm test')
|
||||
const settledTool = screen.getByText('shell')
|
||||
const activity = screen.getByText('Working…')
|
||||
expect(activity.textContent).not.toBe(settledTool.textContent)
|
||||
expect(activity).not.toHaveTextContent('shell')
|
||||
@@ -243,7 +243,8 @@ describe('NativeChatMessageList assistant messages', () => {
|
||||
/>
|
||||
)
|
||||
|
||||
const settledTool = screen.getByText('shell pnpm test')
|
||||
const settledTool = screen.getByText('shell')
|
||||
expect(settledTool).toHaveTextContent('shell pnpm test')
|
||||
expect(settledTool.closest('button')?.querySelector('.animate-pulse')).toBeNull()
|
||||
expect(settledTool.closest('button')?.querySelector('.lucide-check')).toBeInTheDocument()
|
||||
const activity = screen.getByText('Preparing the answer')
|
||||
|
||||
@@ -299,7 +299,7 @@ describe('NativeChatToolRun with a spawn group', () => {
|
||||
)
|
||||
|
||||
expect(screen.getByText('Ran 1 subagent')).toBeInTheDocument()
|
||||
expect(screen.queryByText('shell ls')).toBeNull()
|
||||
expect(screen.queryByText('shell')).toBeNull()
|
||||
})
|
||||
|
||||
it('renders the roster alongside the tool activity of its turn', () => {
|
||||
@@ -313,6 +313,6 @@ describe('NativeChatToolRun with a spawn group', () => {
|
||||
)
|
||||
|
||||
expect(screen.getByText('Ran 1 subagent')).toBeInTheDocument()
|
||||
expect(screen.getByText('shell ls')).toBeInTheDocument()
|
||||
expect(screen.getByText('shell').closest('button')).toHaveTextContent('shell ls')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -70,7 +70,7 @@ describe('inline tool annotations', () => {
|
||||
/>
|
||||
)
|
||||
expect(screen.queryByRole('link')).toBeNull()
|
||||
fireEvent.click(screen.getByText('web_search').closest('button')!)
|
||||
fireEvent.click(screen.getByText('web_search', { selector: 'code' }).closest('button')!)
|
||||
const link = screen.getByRole('link', { name: /Reference docs/ })
|
||||
expect(link.getAttribute('href')).toBe('https://example.com/docs')
|
||||
expect(link.closest('button')).toBeNull()
|
||||
@@ -112,7 +112,10 @@ it.each(['running', 'completed'] as const)(
|
||||
expect(screen.getByText('My server')).toBeTruthy()
|
||||
expect(screen.getByText('ns.tool')).toBeTruthy()
|
||||
expect(screen.getByTitle(name)).toBeTruthy()
|
||||
expect(document.querySelectorAll('.lucide-plug')).toHaveLength(2)
|
||||
// Header glyph plus the row's. A settled header also names each member in a
|
||||
// pill, which carries that member's own glyph — so three, all plug: the
|
||||
// identity holds wherever it is drawn.
|
||||
expect(document.querySelectorAll('.lucide-plug')).toHaveLength(state === 'completed' ? 3 : 2)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -23,6 +23,16 @@ function leadingGlyphs(container: HTMLElement): (string | null)[] {
|
||||
)
|
||||
}
|
||||
|
||||
/** The run header — the first button in a run, above its member rows. Its
|
||||
* members render as separate pills, so it has no single joined summary node. */
|
||||
function runHeader(container: HTMLElement): HTMLElement {
|
||||
const header = container.querySelector('button')
|
||||
if (!header) {
|
||||
throw new Error('run header did not render')
|
||||
}
|
||||
return header
|
||||
}
|
||||
|
||||
describe('NativeChatToolRun', () => {
|
||||
it('uses the shared clean label for a desktop tool row', () => {
|
||||
const blocks: NativeChatBlock[] = [
|
||||
@@ -244,6 +254,94 @@ describe('NativeChatToolRun', () => {
|
||||
expect(writeClipboardText).toHaveBeenCalledWith(' ctx\n-was\n+now\n+tail')
|
||||
})
|
||||
|
||||
describe('reading a batch as a group', () => {
|
||||
const batch: NativeChatBlock[] = [
|
||||
{
|
||||
type: 'tool-call',
|
||||
name: 'mcp__linear__list_issues',
|
||||
input: { query: 'todo' },
|
||||
state: 'completed',
|
||||
mcpIdentity: { server: 'linear', tool: 'list_issues' }
|
||||
},
|
||||
{ type: 'tool-call', name: 'Bash', input: { command: 'ls -la' }, state: 'completed' },
|
||||
{
|
||||
type: 'tool-call',
|
||||
name: 'tools/read',
|
||||
input: { file_path: 'README.md' },
|
||||
state: 'completed'
|
||||
}
|
||||
]
|
||||
|
||||
it('gives each member its own bounded pill instead of one joined string', () => {
|
||||
const { container } = render(<NativeChatToolRun blocks={batch} expandSignal={false} />)
|
||||
|
||||
const pills = runHeader(container).querySelectorAll('.bg-accent')
|
||||
expect([...pills].map((pill) => pill.textContent)).toEqual([
|
||||
'mcp__linear__list_issues todo',
|
||||
'Bash ls -la',
|
||||
'tools/read README.md'
|
||||
])
|
||||
})
|
||||
|
||||
// The header still prints the raw identifier while the row beneath it prints
|
||||
// the split MCP name. Pinned, not endorsed: reconciling the two changes what
|
||||
// a tool is called, which is a naming decision rather than a layout one.
|
||||
it('leaves the header naming a member differently from the row below it', () => {
|
||||
const { container } = render(<NativeChatToolRun blocks={batch} expandSignal />)
|
||||
|
||||
expect(runHeader(container)).toHaveTextContent('mcp__linear__list_issues')
|
||||
expect(screen.getByText('Linear')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('names each member with its own glyph, not the run-wide fallback', () => {
|
||||
const { container } = render(<NativeChatToolRun blocks={batch} expandSignal={false} />)
|
||||
|
||||
const header = runHeader(container)
|
||||
expect(header.querySelector('.bg-accent .lucide-plug')).toBeInTheDocument()
|
||||
expect(header.querySelector('.bg-accent .lucide-square-terminal')).toBeInTheDocument()
|
||||
// The run-wide glyph still reads generic, the categories being mixed.
|
||||
expect(header.firstElementChild?.querySelector('.lucide-wrench')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('counts the members it could not show rather than ending mid-name', () => {
|
||||
const wide: NativeChatBlock[] = [
|
||||
...batch,
|
||||
{ type: 'tool-call', name: 'Grep', input: { pattern: 'todo' }, state: 'completed' },
|
||||
{ type: 'tool-call', name: 'Write', input: { file_path: 'a.ts' }, state: 'completed' }
|
||||
]
|
||||
|
||||
const { container } = render(<NativeChatToolRun blocks={wide} expandSignal={false} />)
|
||||
|
||||
expect(runHeader(container)).toHaveTextContent('+2 more')
|
||||
expect(runHeader(container).querySelectorAll('.bg-accent')).toHaveLength(3)
|
||||
})
|
||||
|
||||
it('leaves no remainder marker when every member is shown', () => {
|
||||
const { container } = render(<NativeChatToolRun blocks={batch} expandSignal={false} />)
|
||||
|
||||
expect(runHeader(container)).not.toHaveTextContent('more')
|
||||
})
|
||||
|
||||
it('indents opened members so the run has a visible end', () => {
|
||||
const { container } = render(<NativeChatToolRun blocks={batch} expandSignal />)
|
||||
|
||||
const members = runHeader(container).parentElement?.querySelector('.pl-4')
|
||||
expect(members).toBeInTheDocument()
|
||||
expect(members?.querySelectorAll('button').length).toBe(batch.length)
|
||||
})
|
||||
|
||||
it('falls back to the call count when a run names no tool', () => {
|
||||
const { container } = render(
|
||||
<NativeChatToolRun
|
||||
blocks={[{ type: 'tool-call', name: ' ', input: {}, state: 'completed' }]}
|
||||
expandSignal={false}
|
||||
/>
|
||||
)
|
||||
|
||||
expect(runHeader(container)).toHaveTextContent('1 tool call')
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps a grouped active run to one stable row showing only the latest tool', () => {
|
||||
const blocks: NativeChatBlock[] = [
|
||||
{ type: 'tool-call', name: 'shell', input: { command: 'date' }, state: 'completed' },
|
||||
@@ -293,7 +391,9 @@ describe('NativeChatToolRun', () => {
|
||||
const runningBlocks: NativeChatBlock[] = [
|
||||
{ type: 'tool-call', name: 'shell', input: { command: 'sleep 1' }, state: 'running' }
|
||||
]
|
||||
const { rerender } = render(<NativeChatToolRun blocks={runningBlocks} expandSignal={false} />)
|
||||
const { rerender, container } = render(
|
||||
<NativeChatToolRun blocks={runningBlocks} expandSignal={false} />
|
||||
)
|
||||
|
||||
expect(screen.getByText('Running sleep 1')).toBeInTheDocument()
|
||||
|
||||
@@ -308,7 +408,7 @@ describe('NativeChatToolRun', () => {
|
||||
)
|
||||
|
||||
expect(screen.queryByText('Running sleep 1')).toBeNull()
|
||||
expect(screen.getByText('shell sleep 1')).toBeInTheDocument()
|
||||
expect(runHeader(container)).toHaveTextContent('shell sleep 1')
|
||||
})
|
||||
|
||||
it('never animates a settled tool row with its completion check', () => {
|
||||
@@ -323,9 +423,10 @@ describe('NativeChatToolRun', () => {
|
||||
/>
|
||||
)
|
||||
|
||||
const settledRow = screen.getByText('shell pnpm test').closest('button')
|
||||
expect(settledRow?.querySelector('.lucide-check')).toBeInTheDocument()
|
||||
expect(settledRow?.querySelector('.animate-pulse')).toBeNull()
|
||||
const settledRow = runHeader(container)
|
||||
expect(settledRow).toHaveTextContent('shell pnpm test')
|
||||
expect(settledRow.querySelector('.lucide-check')).toBeInTheDocument()
|
||||
expect(settledRow.querySelector('.animate-pulse')).toBeNull()
|
||||
expect(container.querySelector('.animate-pulse')).toBeNull()
|
||||
})
|
||||
|
||||
@@ -348,7 +449,7 @@ describe('NativeChatToolRun', () => {
|
||||
{ type: 'tool-result', output: 'exit 128', isError: true }
|
||||
]
|
||||
|
||||
const { rerender } = render(
|
||||
const { rerender, container } = render(
|
||||
<NativeChatToolRun
|
||||
blocks={blocks}
|
||||
expandSignal={false}
|
||||
@@ -369,7 +470,7 @@ describe('NativeChatToolRun', () => {
|
||||
/>
|
||||
)
|
||||
|
||||
expect(screen.getByText('shell git log -1')).toBeInTheDocument()
|
||||
expect(runHeader(container)).toHaveTextContent('shell git log -1')
|
||||
})
|
||||
|
||||
it('settles an orphaned running call when its turn lifecycle has ended', () => {
|
||||
@@ -401,7 +502,7 @@ describe('NativeChatToolRun', () => {
|
||||
const glyph = container.querySelector('.lucide-eye')
|
||||
expect(glyph).toBeInTheDocument()
|
||||
expect(glyph).toHaveAttribute('aria-hidden')
|
||||
expect(screen.getByText('read')).toBeInTheDocument()
|
||||
expect(screen.getByText('read', { selector: 'code' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('holds one glyph for a category across running, completed, and failed', () => {
|
||||
|
||||
@@ -26,8 +26,9 @@ import type { NativeChatEditFile } from '../../../../shared/native-chat-edit-mod
|
||||
import {
|
||||
countToolCalls,
|
||||
createToolInputDisplay,
|
||||
summarizeToolRun,
|
||||
truncateToolDetail
|
||||
toolRunSummaryMembers,
|
||||
truncateToolDetail,
|
||||
type ToolRunMember
|
||||
} from './native-chat-tool-summary'
|
||||
import {
|
||||
NATIVE_CHAT_TOOL_ACTIVITY_COPY,
|
||||
@@ -237,7 +238,22 @@ export function NativeChatToolRun({
|
||||
.filter(isRenderableSubagentGroup)
|
||||
.map((group) => <NativeChatSubagentRun key={group.groupId} block={group} />)
|
||||
const callCount = countToolCalls(blocks) || blocks.length
|
||||
const summary = summarizeToolRun(blocks)
|
||||
// Members stay separate all the way to the markup: joining them into one
|
||||
// string is what made a run read as a single call, because the separator also
|
||||
// occurs inside tool names like `browser.open` and `tools/read`.
|
||||
const summaryMembers = toolRunSummaryMembers(blocks)
|
||||
const hiddenCallCount = Math.max(0, callCount - summaryMembers.length)
|
||||
// Same content-signature keying the member rows below use: two identical calls
|
||||
// in one run are distinguished by occurrence, never by list position.
|
||||
const keyedSummaryMembers = ((): (ToolRunMember & { key: string })[] => {
|
||||
const seen = new Map<string, number>()
|
||||
return summaryMembers.map((member) => {
|
||||
const signature = `${member.name}:${member.arg}`
|
||||
const occurrence = seen.get(signature) ?? 0
|
||||
seen.set(signature, occurrence + 1)
|
||||
return { ...member, key: `${signature}:${occurrence}` }
|
||||
})
|
||||
})()
|
||||
const latestActiveCall = structuredActivityUi
|
||||
? selectActiveToolCall(blocks, { activeTurnIsWorking })
|
||||
: null
|
||||
@@ -251,7 +267,7 @@ export function NativeChatToolRun({
|
||||
() => (open ? buildEditCards(blocks) : NO_EDIT_CARDS),
|
||||
[open, blocks]
|
||||
)
|
||||
// Only the settled header reads this. It stands over `summary`, which speaks
|
||||
// Only the settled header reads this. It stands over `summaryMembers`, which speaks
|
||||
// for the run's first calls rather than its last, so a glyph taken from one
|
||||
// call would assert a category the text beside it doesn't describe. A run that
|
||||
// spans categories therefore heads with the generic tool glyph. The glyph is
|
||||
@@ -329,9 +345,45 @@ export function NativeChatToolRun({
|
||||
<span className="shrink-0 font-mono text-[11px] font-bold text-muted-foreground transition-colors group-hover:text-foreground/80">
|
||||
{callCount}×
|
||||
</span>
|
||||
<span className="min-w-0 truncate font-mono text-[11px] text-muted-foreground transition-colors group-hover:text-foreground/80">
|
||||
{summary || fallbackLabel}
|
||||
</span>
|
||||
{summaryMembers.length > 0 ? (
|
||||
/* Wraps rather than truncates: a clipped member name is the one thing
|
||||
the header cannot afford to lose. */
|
||||
<span className="flex min-w-0 flex-1 flex-wrap items-center gap-1">
|
||||
{keyedSummaryMembers.map((member) => (
|
||||
<span
|
||||
key={member.key}
|
||||
className="flex min-w-0 max-w-72 items-center gap-1 rounded-sm bg-accent px-1.5 py-px font-mono text-[11px] text-foreground/80"
|
||||
>
|
||||
<NativeChatToolIcon
|
||||
rowWord={member.name}
|
||||
mcpIdentity={member.mcpIdentity}
|
||||
className="size-3.5 text-muted-foreground"
|
||||
/>
|
||||
{/* Name and argument share one text run so the pill still reads
|
||||
as `name arg` to a screen reader and to a text selection. */}
|
||||
<span className="truncate">
|
||||
{member.name}
|
||||
{member.arg ? (
|
||||
<span className="text-muted-foreground">{` ${member.arg}`}</span>
|
||||
) : null}
|
||||
</span>
|
||||
</span>
|
||||
))}
|
||||
{hiddenCallCount > 0 ? (
|
||||
<span className="shrink-0 font-mono text-[11px] text-muted-foreground">
|
||||
{translate(
|
||||
'components.native-chat.tool.moreCalls',
|
||||
NATIVE_CHAT_TOOL_ACTIVITY_COPY.moreCalls,
|
||||
{ value0: hiddenCallCount }
|
||||
)}
|
||||
</span>
|
||||
) : null}
|
||||
</span>
|
||||
) : (
|
||||
<span className="min-w-0 flex-1 truncate font-mono text-[11px] text-muted-foreground transition-colors group-hover:text-foreground/80">
|
||||
{fallbackLabel}
|
||||
</span>
|
||||
)}
|
||||
{/* Completion reads as a trailing mark so the leading glyph can stay fixed. */}
|
||||
{structuredActivityUi ? (
|
||||
<Check aria-hidden className="size-3 shrink-0 text-muted-foreground" />
|
||||
@@ -346,7 +398,10 @@ export function NativeChatToolRun({
|
||||
</button>
|
||||
)}
|
||||
{open ? (
|
||||
<div className="mt-1">
|
||||
// Members are indented under the header because nothing else marks the
|
||||
// run's extent — flush rows are indistinguishable from the blocks after
|
||||
// them, so the batch has no visible end.
|
||||
<div className="mt-1 pl-4">
|
||||
{(() => {
|
||||
const seen = new Map<string, number>()
|
||||
return blocks.map((block) => {
|
||||
|
||||
@@ -6,5 +6,7 @@ export {
|
||||
summarizeToolInput,
|
||||
summarizeToolRun,
|
||||
toolFilePath,
|
||||
toolRunSummaryMembers,
|
||||
truncateToolDetail
|
||||
} from '../../../../shared/native-chat-tool-summary'
|
||||
export type { ToolRunMember } from '../../../../shared/native-chat-tool-summary'
|
||||
|
||||
@@ -17039,6 +17039,7 @@
|
||||
"result": "Result",
|
||||
"countOne": "1 tool call",
|
||||
"countN": "{{value0}} tool calls",
|
||||
"moreCalls": "+{{value0}} more",
|
||||
"runningPreview": "Running {{preview}}",
|
||||
"runningCommand": "Running command",
|
||||
"runningNamedPreview": "Running {{toolName}} {{preview}}",
|
||||
|
||||
@@ -13,7 +13,8 @@ export const NATIVE_CHAT_TOOL_ACTIVITY_COPY = {
|
||||
runningNamedPreview: 'Running {{toolName}} {{preview}}',
|
||||
runningNamed: 'Running {{toolName}}',
|
||||
countOne: '1 tool call',
|
||||
countN: '{{value0}} tool calls'
|
||||
countN: '{{value0}} tool calls',
|
||||
moreCalls: '+{{value0}} more'
|
||||
} as const
|
||||
|
||||
/** Tools whose call is a shell command, so the row reads as terminal activity
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
MAX_TOOL_DETAIL_LENGTH,
|
||||
summarizeToolInput,
|
||||
summarizeToolRun,
|
||||
toolRunSummaryMembers,
|
||||
toolFilePath,
|
||||
truncateToolDetail
|
||||
} from './native-chat-tool-summary'
|
||||
@@ -254,6 +255,73 @@ describe('briefToolArg', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('toolRunSummaryMembers', () => {
|
||||
it('keeps each call separate so a boundary can be drawn between them', () => {
|
||||
const blocks: NativeChatBlock[] = [
|
||||
{ type: 'tool-call', name: 'Bash', input: { command: 'ls -la' } },
|
||||
{ type: 'tool-call', name: 'tools/read', input: { file_path: 'README.md' } }
|
||||
]
|
||||
|
||||
expect(toolRunSummaryMembers(blocks)).toEqual([
|
||||
{ name: 'Bash', arg: 'ls -la', mcpIdentity: undefined },
|
||||
{ name: 'tools/read', arg: 'README.md', mcpIdentity: undefined }
|
||||
])
|
||||
})
|
||||
|
||||
// `url` is a PRIMARY_ARG_KEY but not a BRIEF_ARG_KEY, so a call carrying only
|
||||
// a url falls through to the bounded JSON preview and is cut at 28 chars —
|
||||
// mid-token, brace unbalanced. Pinned as-is: the header renders whatever this
|
||||
// returns, and changing the key list would move mobile's summary too.
|
||||
it('still falls through to a clipped JSON preview for a url-only call', () => {
|
||||
const blocks: NativeChatBlock[] = [
|
||||
{ type: 'tool-call', name: 'browser.open', input: { url: 'https://example.com' } }
|
||||
]
|
||||
|
||||
expect(toolRunSummaryMembers(blocks)[0]?.arg).toBe('{"url":"https://example.com"')
|
||||
})
|
||||
|
||||
it('caps at the summary limit and skips nameless calls, as the joined string does', () => {
|
||||
const blocks: NativeChatBlock[] = [
|
||||
{ type: 'tool-call', name: ' ', input: {} },
|
||||
{ type: 'tool-call', name: 'Bash', input: { command: 'ls' } },
|
||||
{ type: 'tool-call', name: 'Read', input: { file_path: 'a.ts' } },
|
||||
{ type: 'tool-call', name: 'Edit', input: { file_path: 'b.ts' } },
|
||||
{ type: 'tool-call', name: 'Write', input: { file_path: 'c.ts' } }
|
||||
]
|
||||
|
||||
const members = toolRunSummaryMembers(blocks)
|
||||
expect(members.map((member) => member.name)).toEqual(['Bash', 'Read', 'Edit'])
|
||||
// The joined string is derived from these, so the two can never disagree.
|
||||
expect(summarizeToolRun(blocks)).toBe(
|
||||
members.map((member) => `${member.name} ${member.arg}`).join(' · ')
|
||||
)
|
||||
})
|
||||
|
||||
it('carries provider MCP identity through, so a pill can draw the server glyph', () => {
|
||||
const blocks: NativeChatBlock[] = [
|
||||
{
|
||||
type: 'tool-call',
|
||||
name: 'mcp__linear__list_issues',
|
||||
input: {},
|
||||
mcpIdentity: { server: 'linear', tool: 'list_issues' }
|
||||
}
|
||||
]
|
||||
|
||||
expect(toolRunSummaryMembers(blocks)[0]?.mcpIdentity).toEqual({
|
||||
server: 'linear',
|
||||
tool: 'list_issues'
|
||||
})
|
||||
})
|
||||
|
||||
it('reports a blank argument rather than standing raw JSON in for one', () => {
|
||||
const blocks: NativeChatBlock[] = [{ type: 'tool-call', name: 'Bash', input: { command: '' } }]
|
||||
|
||||
expect(toolRunSummaryMembers(blocks)).toEqual([
|
||||
{ name: 'Bash', arg: '', mcpIdentity: undefined }
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('summarizeToolRun', () => {
|
||||
it('caps the run summary and skips nameless calls', () => {
|
||||
const blocks: NativeChatBlock[] = [
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { NativeChatMcpIdentity } from './native-chat-tool-identity'
|
||||
import { isToolCallBlock, type NativeChatBlock } from './native-chat-types'
|
||||
|
||||
const MAX_PREVIEW_LENGTH = 80
|
||||
@@ -260,8 +261,20 @@ function summarizePrimaryToolArg(input: unknown): string | null {
|
||||
return null
|
||||
}
|
||||
|
||||
export function summarizeToolRun(blocks: readonly NativeChatBlock[]): string {
|
||||
const parts: string[] = []
|
||||
/** One named call in a run header, kept apart rather than pre-joined so a
|
||||
* surface can draw the boundary between members itself. */
|
||||
export type ToolRunMember = {
|
||||
name: string
|
||||
/** Brief argument, or '' when the call has none worth showing. */
|
||||
arg: string
|
||||
mcpIdentity?: NativeChatMcpIdentity
|
||||
}
|
||||
|
||||
/** The run header's leading calls. Capped at the same limit the joined string
|
||||
* has always used, so the two can never disagree about which calls speak for
|
||||
* a run. */
|
||||
export function toolRunSummaryMembers(blocks: readonly NativeChatBlock[]): ToolRunMember[] {
|
||||
const members: ToolRunMember[] = []
|
||||
for (const block of blocks) {
|
||||
if (!isToolCallBlock(block)) {
|
||||
continue
|
||||
@@ -270,13 +283,18 @@ export function summarizeToolRun(blocks: readonly NativeChatBlock[]): string {
|
||||
if (!name) {
|
||||
continue
|
||||
}
|
||||
const detail = briefToolArg(block.input)
|
||||
parts.push(detail ? `${name} ${detail}` : name)
|
||||
if (parts.length >= MAX_TOOL_RUN_SUMMARY_PARTS) {
|
||||
members.push({ name, arg: briefToolArg(block.input), mcpIdentity: block.mcpIdentity })
|
||||
if (members.length >= MAX_TOOL_RUN_SUMMARY_PARTS) {
|
||||
break
|
||||
}
|
||||
}
|
||||
return parts.join(' · ')
|
||||
return members
|
||||
}
|
||||
|
||||
export function summarizeToolRun(blocks: readonly NativeChatBlock[]): string {
|
||||
return toolRunSummaryMembers(blocks)
|
||||
.map((member) => (member.arg ? `${member.name} ${member.arg}` : member.name))
|
||||
.join(' · ')
|
||||
}
|
||||
|
||||
export function countToolCalls(blocks: readonly NativeChatBlock[]): number {
|
||||
|
||||
Reference in New Issue
Block a user