Skip to content

Commit 2421e33

Browse files
committed
improvement(chat): simplify inline tool activity
1 parent 8d9875c commit 2421e33

10 files changed

Lines changed: 283 additions & 153 deletions

File tree

apps/sim/app/(landing)/components/hero/components/hero-chat-loop/hero-chat-loop.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,6 @@ export function HeroChatLoop({
217217
agentLabel='Workflow Agent'
218218
items={WORKFLOW_AGENT_BUILDING_ITEMS}
219219
isStreaming
220-
isCurrentSection
221220
isLaneOpen
222221
defaultExpanded
223222
autoScrollActivity={false}
@@ -237,7 +236,6 @@ export function HeroChatLoop({
237236
agentName='mothership'
238237
agentLabel='Sim'
239238
items={SIM_ITEMS}
240-
defaultExpanded
241239
/>
242240
<HeroChatReply
243241
content={replyComplete ? replyMessage : replyWords.slice(0, revealedWords).join('')}

apps/sim/app/(landing)/components/hero/components/hero-chat-loop/hero-tool-call-item.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { Table } from '@sim/emcn/icons'
22
import { SlackIcon } from '@/components/icons'
3+
import { ActivityStatus } from '@/components/ui/activity-status'
34
import { getToolStatusDisplayTitle } from '@/lib/copilot/tools/tool-display'
45
import type { ToolCallItemProps } from '@/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-call-item'
5-
import { ToolCallRow } from '@/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-call-row'
66

77
/** Demo fixtures have known brands, so the landing page never loads the block registry. */
88
export function HeroToolCallItem({
@@ -18,9 +18,9 @@ export function HeroToolCallItem({
1818
? Table
1919
: undefined
2020
return (
21-
<ToolCallRow
22-
title={getToolStatusDisplayTitle(displayTitle, status, toolName)}
23-
isExecuting={status === 'executing'}
21+
<ActivityStatus
22+
label={getToolStatusDisplayTitle(displayTitle, status, toolName)}
23+
isActive={status === 'executing'}
2424
icon={Icon && <Icon className='size-[14px] shrink-0 text-[var(--text-icon)]' />}
2525
/>
2626
)

apps/sim/app/workspace/[workspaceId]/home/components/knowledge-search-results/knowledge-search-results.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import { useMemo } from 'react'
44
import { Chip, ChipLink } from '@sim/emcn'
55
import { useQueryStates } from 'nuqs'
6+
import { ShimmerText } from '@/components/ui/shimmer-text'
67
import type {
78
WorkspaceKnowledgeSearchResult,
89
WorkspaceSearchFilters,
@@ -188,7 +189,11 @@ export function KnowledgeSearchResults({
188189
)
189190
}
190191
if (isPending || (isFetching && !results)) {
191-
return <p className='px-2 py-2 text-[var(--text-muted)] text-caption'>Searching…</p>
192+
return (
193+
<p role='status' className='px-2 py-2 text-[var(--text-muted)] text-caption'>
194+
<ShimmerText className='[--shimmer-rest:var(--text-muted)]'>Searching…</ShimmerText>
195+
</p>
196+
)
192197
}
193198

194199
const indexingNote =

apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group-view.tsx

Lines changed: 76 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
import { ChevronDown, cn, Expandable, ExpandableContent, OverflowText } from '@sim/emcn'
1313
import { ShimmerText } from '@/components/ui'
1414
import { isBrowserAgentAvailable } from '@/lib/browser-agent/transport'
15+
import { Terminal as TerminalTool } from '@/lib/copilot/generated/tool-catalog-v1'
1516
import { RETIRED_BROWSER_REQUEST_TAKEOVER_ID } from '@/lib/copilot/tools/retired-tools'
1617
import { renderInlineMarkdown } from '@/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/inline-markdown'
1718
import type { ToolCallItemProps } from '@/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-call-item'
@@ -47,11 +48,9 @@ export interface AgentGroupProps {
4748
items: AgentGroupItem[]
4849
isDelegating?: boolean
4950
isStreaming?: boolean
50-
/** This group is the latest section in its parent sequence (drives collapse). */
51-
isCurrentSection?: boolean
5251
/** The subagent lane is still open (no subagent_end yet) — i.e. actively running. */
5352
isLaneOpen?: boolean
54-
/** Opens the group on first render without changing production's automatic collapse rules. */
53+
/** Opens a subagent group on first render. */
5554
defaultExpanded?: boolean
5655
/** Keeps the activity viewport anchored at the top while new rows stream in. */
5756
autoScrollActivity?: boolean
@@ -151,7 +150,6 @@ export function AgentGroupView({
151150
items,
152151
isDelegating = false,
153152
isStreaming = false,
154-
isCurrentSection = false,
155153
isLaneOpen = false,
156154
defaultExpanded = false,
157155
autoScrollActivity = true,
@@ -192,26 +190,15 @@ export function AgentGroupView({
192190
const isWorking =
193191
!activeBrowserTakeover && ((isDelegating && !resolved) || (isStreaming && isLaneOpen))
194192

195-
// SUBAGENT groups never auto-expand: the collapsed row IS the live view —
196-
// label plus latest running tool title. Expanding is a deliberate user
197-
// action; only a pending permission prompt or a browser hand-back forces
198-
// one open. The MAIN lane ("Sim") is not a delegation card: its narration
199-
// and tool calls are the turn itself, so it keeps the original live-expand
200-
// behavior (open while streaming/current, settles when superseded).
201-
const autoExpanded = isMainAgent && isStreaming && (isCurrentSection || isLaneOpen || !resolved)
202-
const [manualExpanded, setManualExpanded] = useState<boolean | null>(
203-
defaultExpanded ? true : null
204-
)
193+
const [manualExpanded, setManualExpanded] = useState(defaultExpanded)
205194
const [expandedTakeoverId, setExpandedTakeoverId] = useState<string | null>(null)
206195
// An outstanding permission prompt overrides a manual collapse: the turn
207196
// cannot proceed until it is answered, so hiding it would deadlock the chat
208197
// with nothing on screen to explain why.
209198
const expanded =
210199
hasAwaitingApproval(items) ||
211200
nestedBrowserTakeover ||
212-
(activeBrowserTakeover
213-
? expandedTakeoverId === activeBrowserTakeover.id
214-
: (manualExpanded ?? autoExpanded))
201+
(activeBrowserTakeover ? expandedTakeoverId === activeBrowserTakeover.id : manualExpanded)
215202

216203
const toggleExpanded = () => {
217204
if (activeBrowserTakeover) {
@@ -221,9 +208,74 @@ export function AgentGroupView({
221208
setManualExpanded(!expanded)
222209
}
223210

211+
let latestTool: AgentGroupItem | undefined
212+
if (isMainAgent) {
213+
for (let index = items.length - 1; index >= 0; index--) {
214+
if (items[index].type === 'tool') {
215+
latestTool = items[index]
216+
break
217+
}
218+
}
219+
}
220+
/** Keep blocking controls visible even when a newer tool replaces the activity text. */
221+
const visibleItems = isMainAgent
222+
? items.filter(
223+
(item) =>
224+
item.type !== 'tool' ||
225+
item === latestTool ||
226+
item.data.status === ToolCallStatus.awaiting_approval ||
227+
(item.data.status === ToolCallStatus.executing &&
228+
item.data.toolName === TerminalTool.id &&
229+
item.data.params?.operation === 'handoff')
230+
)
231+
: items
232+
const activity = (
233+
<div className={cn('flex min-w-0 flex-col gap-1.5', !isMainAgent && 'py-0.5 pl-6')}>
234+
{visibleItems.map((item, idx) => {
235+
if (item.type === 'tool') {
236+
return (
237+
<ToolCallComponent
238+
key={item.data.id}
239+
toolCallId={item.data.id}
240+
toolName={item.data.toolName}
241+
displayTitle={item.data.displayTitle}
242+
status={item.data.status}
243+
params={item.data.params}
244+
result={item.data.result}
245+
streamingArgs={item.data.streamingArgs}
246+
startedAt={item.data.startedAt}
247+
/>
248+
)
249+
}
250+
if (item.type === 'agent_group') {
251+
return (
252+
<AgentGroupView
253+
key={item.group.id}
254+
ToolCallComponent={ToolCallComponent}
255+
renderBrowserTakeover={renderBrowserTakeover}
256+
agentName={item.group.agentName}
257+
agentLabel={item.group.agentLabel}
258+
items={item.group.items}
259+
isDelegating={item.group.isDelegating}
260+
isStreaming={isStreaming}
261+
isLaneOpen={item.group.isOpen}
262+
/>
263+
)
264+
}
265+
return (
266+
<NarrationText
267+
key={`text-${idx}`}
268+
content={item.content}
269+
isStreaming={isStreaming && idx === visibleItems.length - 1}
270+
/>
271+
)
272+
})}
273+
</div>
274+
)
275+
224276
return (
225277
<div className='flex flex-col gap-1.5'>
226-
{hasItems ? (
278+
{isMainAgent ? null : hasItems ? (
227279
<button
228280
type='button'
229281
onClick={toggleExpanded}
@@ -256,60 +308,20 @@ export function AgentGroupView({
256308
)}
257309
</div>
258310
)}
259-
{hasItems && (
311+
{isMainAgent ? (
312+
activity
313+
) : hasItems ? (
260314
<Expandable expanded={expanded}>
261315
<ExpandableContent>
262316
<BoundedViewport
263317
isStreaming={isStreaming && autoScrollActivity}
264318
unbounded={nestedBrowserTakeover}
265319
>
266-
<div className='flex flex-col gap-1.5 py-0.5'>
267-
{items.map((item, idx) => {
268-
if (item.type === 'tool') {
269-
return (
270-
<ToolCallComponent
271-
key={item.data.id}
272-
toolCallId={item.data.id}
273-
toolName={item.data.toolName}
274-
displayTitle={item.data.displayTitle}
275-
status={item.data.status}
276-
params={item.data.params}
277-
result={item.data.result}
278-
streamingArgs={item.data.streamingArgs}
279-
startedAt={item.data.startedAt}
280-
/>
281-
)
282-
}
283-
if (item.type === 'agent_group') {
284-
return (
285-
<div key={item.group.id} className='pl-6'>
286-
<AgentGroupView
287-
ToolCallComponent={ToolCallComponent}
288-
renderBrowserTakeover={renderBrowserTakeover}
289-
agentName={item.group.agentName}
290-
agentLabel={item.group.agentLabel}
291-
items={item.group.items}
292-
isDelegating={item.group.isDelegating}
293-
isStreaming={isStreaming}
294-
isCurrentSection={idx === items.length - 1}
295-
isLaneOpen={item.group.isOpen}
296-
/>
297-
</div>
298-
)
299-
}
300-
return (
301-
<NarrationText
302-
key={`text-${idx}`}
303-
content={item.content}
304-
isStreaming={isStreaming && idx === items.length - 1}
305-
/>
306-
)
307-
})}
308-
</div>
320+
{activity}
309321
</BoundedViewport>
310322
</ExpandableContent>
311323
</Expandable>
312-
)}
324+
) : null}
313325
{activeBrowserTakeover && (
314326
<div key={activeBrowserTakeover.id} className='animate-stream-fade-in'>
315327
{renderBrowserTakeover?.(activeBrowserTakeover.reason)}
@@ -334,7 +346,7 @@ function NarrationText({ content, isStreaming }: NarrationTextProps) {
334346
const revealed = useSmoothText(content, isStreaming)
335347

336348
return (
337-
<span className='pl-6 text-[13px] text-[var(--text-muted)] leading-[18px]'>
349+
<span className='text-[13px] text-[var(--text-muted)] leading-[18px]'>
338350
{renderInlineMarkdown(revealed.trim())}
339351
</span>
340352
)

0 commit comments

Comments
 (0)