Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export function HeroToolCallItem({
renderStatus,
toolName,
displayTitle,
activityDescription,
status,
}: ToolCallItemProps) {
const Icon =
Expand All @@ -21,7 +22,7 @@ export function HeroToolCallItem({
: getToolIcon(toolName)
const activity = (
<ActivityStatus
label={getToolStatusDisplayTitle(displayTitle, status, toolName)}
label={getToolStatusDisplayTitle(displayTitle, status, toolName, activityDescription)}
isActive={status === 'executing'}
icon={<Icon className='size-full' />}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { type ComponentType, type ReactNode, useMemo, useState } from 'react'
import { ActivityStatus } from '@/components/ui/activity-status'
import { isBrowserAgentAvailable } from '@/lib/browser-agent/transport'
import { RETIRED_BROWSER_REQUEST_TAKEOVER_ID } from '@/lib/copilot/tools/retired-tools'
import { getToolStatusDisplayTitle } from '@/lib/copilot/tools/tool-display'
import { ActivityDisclosure } from '@/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/activity-disclosure'
import { BrowserAgentIcon } from '@/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/browser-agent-icon'
import { renderInlineMarkdown } from '@/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/inline-markdown'
Expand Down Expand Up @@ -52,7 +53,12 @@ export interface AgentGroupProps {
}

function toolStatusTitle(tool: ToolCallData): string {
return tool.displayTitle || String(tool.toolName ?? '')
return getToolStatusDisplayTitle(
tool.displayTitle || String(tool.toolName ?? ''),
tool.status,
tool.toolName,
tool.activityDescription
)
}

/**
Expand Down Expand Up @@ -213,6 +219,7 @@ export function AgentGroupView({
toolCallId={item.data.id}
toolName={item.data.toolName}
displayTitle={item.data.displayTitle}
activityDescription={item.data.activityDescription}
status={item.data.status}
params={item.data.params}
result={item.data.result}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,43 @@ describe('AgentGroup inline main activity', () => {
container.remove()
})

it.each(['mothership', 'workflow', 'browser'])(
'uses the same model description in the %s live header and expanded row',
(agentName) => {
act(() =>
root.render(
createElement(AgentGroup, {
agentName,
agentLabel: agentName,
defaultExpanded: true,
isLaneOpen: true,
isStreaming: true,
items: [
{
type: 'tool',
data: {
id: 'described-read',
toolName: 'read',
displayTitle: 'Reading files',
activityDescription: 'Checking the project timeline',
status: 'executing',
},
},
],
})
)
)

const statuses = [...container.querySelectorAll('[role="status"]')]
expect(statuses).toHaveLength(agentName === 'mothership' ? 1 : 2)
for (const status of statuses) {
expect(status.textContent).toContain('Checking the project timeline')
}
expect(container.textContent).not.toContain('Reading files')
expect(container.querySelector('[class*="shimmer"]')).not.toBeNull()
}
)

it.each([
['executing', 'Reading notes'],
['success', 'Read notes'],
Expand Down Expand Up @@ -733,6 +770,6 @@ describe('AgentGroup nested status line', () => {
namedTool('Reading workflow', 'success' as ToolCallStatus, 1),
group([namedTool('Deploying Invoice Sync as API', 'success' as ToolCallStatus, 2)]),
])
expect(header).toContain('Workflow Agent — Deploying Invoice Sync as API')
expect(header).toContain('Workflow Agent — Deployed Invoice Sync as API')
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,12 @@ const MAX_SUMMARY_ACTIONS = 2
export function getToolActivitySummary(tools: ToolCallData[]): string {
if (tools.length === 1) {
const tool = tools[0]
return getToolStatusDisplayTitle(tool.displayTitle, tool.status, tool.toolName)
return getToolStatusDisplayTitle(
tool.displayTitle,
tool.status,
tool.toolName,
tool.activityDescription
)
}
const labels = new Set<string>()
let failed = 0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,90 @@ describe('ToolCallItem', () => {
expect(markup).not.toContain('Writing brief.md')
})

it.each([
['executing', 'Checking the invoice totals'],
['success', 'Checked the invoice totals'],
['error', 'Failed checking the invoice totals'],
['cancelled', 'Stopped checking the invoice totals'],
['rejected', 'Failed checking the invoice totals'],
['skipped', 'Skipped checking the invoice totals'],
] as const)(
'projects %s from the actual tool status onto the model description',
(status, title) => {
const markup = renderToStaticMarkup(
<ToolCallItem
toolName='prepare_file_edit'
displayTitle='Editing report.md'
activityDescription='Checking the invoice totals'
status={status}
streamingArgs='{"operation":"patch","title":"report.md"}'
/>
)

expect(markup).toContain(title)
expect(markup).not.toContain('report.md')
}
)

it.each([' ', 'a'.repeat(161)])(
'uses the existing title for an invalid description',
(activityDescription) => {
const markup = renderToStaticMarkup(
<ToolCallItem
toolName='grep'
displayTitle='Searching files'
activityDescription={activityDescription}
status='executing'
/>
)

expect(markup).toContain('Searching files')
}
)

it('keeps an executing wait countdown in place of the model phrase', () => {
const markup = renderToStaticMarkup(
<ToolCallItem
toolName='wait'
displayTitle='Waiting'
activityDescription='Waiting for the export'
status='executing'
params={{ seconds: 10 }}
/>
)

expect(markup).toContain('10s')
expect(markup).not.toContain('Waiting for the export')
})

it('renders model descriptions as text, without interpreting markup', () => {
const markup = renderToStaticMarkup(
<ToolCallItem
toolName='read'
displayTitle='Reading a page'
activityDescription='Reading <script>alert(1)</script>'
status='executing'
/>
)

expect(markup).toContain('&lt;script&gt;')
expect(markup).not.toContain('<script>')
})

it('does not let model-authored outcome wording override a failure', () => {
const markup = renderToStaticMarkup(
<ToolCallItem
toolName='read'
displayTitle='Reading a page'
activityDescription='Stopped checking invoices'
status='error'
/>
)

expect(markup).toContain('Failed checking invoices')
expect(markup).not.toContain('Stopped checking invoices')
})

it('defensively applies the completed verb for every successful tool row', () => {
const markup = renderToStaticMarkup(
<ToolCallItem toolName='diff_workflows' displayTitle='Comparing workflows' status='success' />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export function CircleStop({ className }: { className?: string }) {
export interface ToolCallItemProps {
toolName: string
displayTitle: string
activityDescription?: string
status: ToolCallStatus
params?: Record<string, unknown>
result?: ToolCallData['result']
Expand Down Expand Up @@ -125,6 +126,7 @@ function useElapsedMs(
export function ToolCallItem({
toolName,
displayTitle,
activityDescription,
status,
params,
result,
Expand Down Expand Up @@ -188,7 +190,12 @@ export function ToolCallItem({
const liveTitle = isCountingDown
? getWaitCountdownTitle(params, elapsedMs)
: liveWorkspaceFileTitle || displayTitle
const title = getToolStatusDisplayTitle(liveTitle, status, toolName)
const title = getToolStatusDisplayTitle(
liveTitle,
status,
toolName,
isCountingDown ? undefined : activityDescription
)

// A waiting terminal handoff swaps its row for the hand-back chip, the same
// way a browser takeover does: the row would otherwise spin with nothing
Expand All @@ -209,7 +216,7 @@ export function ToolCallItem({
<ToolPermissionCard
toolCallId={toolCallId}
toolName={toolName}
displayTitle={liveTitle}
displayTitle={title}
params={params}
/>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,33 @@ describe('getOrchestratorMessageText', () => {
})

describe('parseBlocks span-identity tree', () => {
it.each(['read', 'respond', 'prepare_file_edit'])(
'prefers invocation intent over %s fallback titles',
(name) => {
const segments = parseBlocks([
{
type: 'tool_call',
toolCall: {
id: 'described-tool',
name,
status: 'success',
displayTitle: 'Fallback title',
activityDescription: ' Checking\nlaunch updates ',
params: { path: 'workspace/files/brief.md' },
},
timestamp: 1,
},
])
const group = segments[0]
if (group.type !== 'agent_group') throw new Error('expected mothership group')
const tool = group.items[0]
if (tool?.type !== 'tool') throw new Error('expected tool activity')
expect(tool.data.displayTitle).toBe('Checked launch updates')
expect(tool.data.activityDescription).toBe('Checking launch updates')
expect(tool.data.params).toEqual({ path: 'workspace/files/brief.md' })
}
)

it('refines a completed credential rename with its previous and new names', () => {
const segments = parseBlocks([
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
getToolDisplayTitle,
getToolStatusDisplayTitle,
humanizeToolName,
normalizeToolActivityDescription,
} from '@/lib/copilot/tools/tool-display'
import { useChatSurface } from '@/app/workspace/[workspaceId]/home/components/chat-surface-context'
import type { CredentialSubmissionPayload } from '@/app/workspace/[workspaceId]/home/components/message-content/components/special-tags'
Expand Down Expand Up @@ -200,15 +201,22 @@ function getOverrideDisplayTitle(tc: NonNullable<ContentBlock['toolCall']>): str
}

function toToolData(tc: NonNullable<ContentBlock['toolCall']>): ToolCallData {
const activityDescription = normalizeToolActivityDescription(tc.activityDescription)
const overrideDisplayTitle = getOverrideDisplayTitle(tc)
const resolvedTitle =
overrideDisplayTitle || tc.displayTitle || getToolDisplayTitle(tc.name, tc.params)
const displayTitle = getToolStatusDisplayTitle(resolvedTitle, tc.status, tc.name)
const displayTitle = getToolStatusDisplayTitle(
resolvedTitle,
tc.status,
tc.name,
activityDescription
)

return {
id: tc.id,
toolName: tc.name,
displayTitle,
activityDescription,
status: tc.status,
params: tc.params,
result: tc.result,
Expand Down
Loading
Loading