From cf34cd2c94992358b7998df418848e110ed4c179 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Sat, 12 Sep 2026 20:15:18 -0700 Subject: [PATCH 1/3] feat(copilot): show model-authored tool activity --- .../hero-chat-loop/hero-tool-call-item.tsx | 3 +- .../agent-group/agent-group-view.tsx | 9 +- .../agent-group/agent-group.test.ts | 39 ++++++- .../agent-group/tool-activity-group.tsx | 7 +- .../agent-group/tool-call-item.test.tsx | 83 ++++++++++++++ .../components/agent-group/tool-call-item.tsx | 11 +- .../message-content/message-content.test.ts | 27 +++++ .../message-content/message-content.tsx | 10 +- .../hooks/stream/turn-model-serialize.test.ts | 101 ++++++++++++++++++ .../home/hooks/stream/turn-model-serialize.ts | 7 +- .../home/hooks/stream/turn-model.ts | 10 +- .../[workspaceId]/home/hooks/use-chat.ts | 6 ++ .../app/workspace/[workspaceId]/home/types.ts | 3 + apps/sim/lib/api/contracts/copilot.ts | 1 + apps/sim/lib/copilot/chat/display-message.ts | 3 + .../copilot/chat/effective-transcript.test.ts | 64 +++++++++++ .../lib/copilot/chat/effective-transcript.ts | 24 +++-- .../copilot/chat/persisted-message.test.ts | 81 ++++++++++++++ .../sim/lib/copilot/chat/persisted-message.ts | 11 ++ .../generated/mothership-stream-v1-schema.ts | 4 + .../copilot/generated/mothership-stream-v1.ts | 1 + .../copilot/request/handlers/handlers.test.ts | 85 +++++++++++++++ apps/sim/lib/copilot/request/handlers/tool.ts | 39 +++++-- apps/sim/lib/copilot/request/types.ts | 2 + .../lib/copilot/tools/tool-display.test.ts | 27 +++++ apps/sim/lib/copilot/tools/tool-display.ts | 35 ++++-- 26 files changed, 659 insertions(+), 34 deletions(-) diff --git a/apps/sim/app/(landing)/components/hero/components/hero-chat-loop/hero-tool-call-item.tsx b/apps/sim/app/(landing)/components/hero/components/hero-chat-loop/hero-tool-call-item.tsx index b659eae8b45..f32b4385062 100644 --- a/apps/sim/app/(landing)/components/hero/components/hero-chat-loop/hero-tool-call-item.tsx +++ b/apps/sim/app/(landing)/components/hero/components/hero-chat-loop/hero-tool-call-item.tsx @@ -11,6 +11,7 @@ export function HeroToolCallItem({ renderStatus, toolName, displayTitle, + activityDescription, status, }: ToolCallItemProps) { const Icon = @@ -21,7 +22,7 @@ export function HeroToolCallItem({ : getToolIcon(toolName) const activity = ( } /> diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group-view.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group-view.tsx index c9733459807..32183aec931 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group-view.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group-view.tsx @@ -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' @@ -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 + ) } /** @@ -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} diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group.test.ts b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group.test.ts index 8dd4c725517..1e0b1923cdc 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group.test.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group.test.ts @@ -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'], @@ -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') }) }) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-activity-group.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-activity-group.tsx index d426fe587da..d86c2fbd3af 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-activity-group.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-activity-group.tsx @@ -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() let failed = 0 diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-call-item.test.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-call-item.test.tsx index b52d7b1b0e6..a67ea10d4de 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-call-item.test.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-call-item.test.tsx @@ -48,6 +48,89 @@ 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( + + ) + + 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( + + ) + + expect(markup).toContain('Searching files') + } + ) + + it('keeps an executing wait countdown in place of the model phrase', () => { + const markup = renderToStaticMarkup( + + ) + + expect(markup).toContain('10s') + expect(markup).not.toContain('Waiting for the export') + }) + + it('renders model descriptions as text, without interpreting markup', () => { + const markup = renderToStaticMarkup( + + ) + + expect(markup).toContain('<script>') + expect(markup).not.toContain('