Skip to content

Commit fb86acf

Browse files
committed
feat(chat): show sub-agent activity and cited sources
1 parent b45a6ba commit fb86acf

2 files changed

Lines changed: 167 additions & 2 deletions

File tree

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
'use client'
2+
3+
import { useState } from 'react'
4+
import { Chip, cn, OverflowText, Popover, PopoverContent, PopoverTrigger } from '@sim/emcn'
5+
import { ChevronRight, List, Users } from '@sim/emcn/icons'
6+
import type { NestedAgentGroup } from '@/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group-view'
7+
import { SourceCard } from '@/app/workspace/[workspaceId]/home/components/message-content/components/source-card'
8+
import { parseBlocks } from '@/app/workspace/[workspaceId]/home/components/message-content/message-content'
9+
import { collectCitedMessageSources } from '@/app/workspace/[workspaceId]/home/components/message-content/message-sources'
10+
import type { ChatMessage } from '@/app/workspace/[workspaceId]/home/types'
11+
12+
type ChatAgentActivity = NestedAgentGroup & { key: string; live: boolean }
13+
14+
interface ChatDetailsProps {
15+
title: string
16+
messages: ChatMessage[]
17+
isSending: boolean
18+
}
19+
20+
/** Live and saved sub-agent activity and cited sources owned by this conversation. */
21+
export function ChatDetails({ title, messages, isSending }: ChatDetailsProps) {
22+
const [open, setOpen] = useState(false)
23+
const [allSources, setAllSources] = useState(false)
24+
const agents: ChatAgentActivity[] = []
25+
const sources = new Map<string, ReturnType<typeof collectCitedMessageSources>[number]>()
26+
for (const [index, message] of messages.entries()) {
27+
if (message.role !== 'assistant') continue
28+
const live = isSending && index === messages.length - 1
29+
for (const segment of parseBlocks(message.contentBlocks ?? [], live)) {
30+
if (segment.type === 'agent_group') {
31+
const collect = (group: NestedAgentGroup) => {
32+
if (group.agentName !== 'mothership')
33+
agents.push({ ...group, key: `${message.id}:${group.id}`, live })
34+
for (const item of group.items) if (item.type === 'agent_group') collect(item.group)
35+
}
36+
collect(segment)
37+
}
38+
}
39+
for (const source of collectCitedMessageSources(message.contentBlocks ?? [], message.content)) {
40+
sources.set(source.url, source)
41+
}
42+
}
43+
const done = agents.filter((agent) => !agent.isOpen && !agent.error).length
44+
const working = agents.filter((agent) => agent.live && agent.isOpen && !agent.error).length
45+
const details = (
46+
<div className='divide-y divide-[var(--border)]'>
47+
<section className='p-4' aria-label='Sub-agents'>
48+
<div className='mb-3 flex items-center justify-between text-small'>
49+
<h3 className='text-[var(--text-muted)]'>Sub-agents</h3>
50+
<span className='text-[var(--text-muted)]'>
51+
{working
52+
? `${working} working${done ? ` · ${done} done` : ''}`
53+
: `${agents.length} total`}
54+
</span>
55+
</div>
56+
{agents.length ? (
57+
agents.map((agent) => (
58+
<details key={agent.key} className='group py-1'>
59+
<summary className='flex cursor-pointer list-none items-center gap-2 rounded-lg px-2 py-2 hover:bg-[var(--surface-hover)]'>
60+
<Users
61+
className={cn(
62+
'size-4 shrink-0',
63+
agent.live && agent.isOpen
64+
? 'text-[var(--brand-blue)]'
65+
: 'text-[var(--text-icon)]'
66+
)}
67+
/>
68+
<OverflowText
69+
label={agent.agentLabel || agent.agentName}
70+
className='min-w-0 flex-1 text-small'
71+
/>
72+
<span className='shrink-0 text-[var(--text-muted)] text-caption'>
73+
{agent.error
74+
? 'Failed'
75+
: agent.isOpen
76+
? agent.live
77+
? 'Working'
78+
: 'Stopped'
79+
: 'Done'}
80+
</span>
81+
<ChevronRight className='size-3 shrink-0 text-[var(--text-icon)] group-open:rotate-90' />
82+
</summary>
83+
<div className='ml-8 border-[var(--border)] border-l px-3 py-2 text-[var(--text-secondary)] text-caption'>
84+
{agent.error ? <p>{agent.error}</p> : null}
85+
{agent.items
86+
.filter((item) => item.type !== 'text')
87+
.map((item, index) => (
88+
<p key={index} className='flex items-center justify-between gap-2 py-1'>
89+
<span>
90+
{item.type === 'tool'
91+
? item.data.displayTitle || item.data.toolName.replaceAll('_', ' ')
92+
: item.group.agentLabel}
93+
</span>
94+
{item.type === 'tool' ? (
95+
<span className='text-[var(--text-muted)]'>
96+
{item.data.status === 'executing'
97+
? 'Working'
98+
: item.data.status === 'success'
99+
? 'Done'
100+
: item.data.status.replaceAll('_', ' ')}
101+
</span>
102+
) : null}
103+
</p>
104+
))}
105+
{!agent.items.some((item) => item.type !== 'text') && !agent.error ? (
106+
<p>No tool activity recorded.</p>
107+
) : null}
108+
</div>
109+
</details>
110+
))
111+
) : (
112+
<p className='text-[var(--text-muted)] text-small'>
113+
Sub-agents will appear here when Sim delegates work.
114+
</p>
115+
)}
116+
</section>
117+
<section className='p-4' aria-label='Chat sources'>
118+
<div className='mb-2 flex items-center justify-between text-[var(--text-muted)] text-small'>
119+
<h3>Sources</h3>
120+
<span>{sources.size}</span>
121+
</div>
122+
{sources.size ? (
123+
<>
124+
{[...sources.values()].slice(0, allSources ? undefined : 5).map((source) => (
125+
<SourceCard key={source.url} source={source} dense />
126+
))}
127+
{sources.size > 5 ? (
128+
<Chip onClick={() => setAllSources(!allSources)}>
129+
{allSources ? 'Show fewer' : `View all ${sources.size} sources`}
130+
</Chip>
131+
) : null}
132+
</>
133+
) : (
134+
<p className='text-[var(--text-muted)] text-small'>
135+
Sources cited in this conversation will appear here.
136+
</p>
137+
)}
138+
</section>
139+
</div>
140+
)
141+
return (
142+
<header className='flex h-[calc(var(--resource-header-controls-height)+1px)] shrink-0 items-center gap-2 border-[var(--border)] border-b px-4'>
143+
<OverflowText label={title} className='min-w-0 flex-1 text-[var(--text-body)] text-small' />
144+
<Popover open={open} onOpenChange={setOpen}>
145+
<PopoverTrigger asChild>
146+
<Chip aria-label='Chat details' active={open}>
147+
<List className='size-4' />
148+
{working > 0 ? <span className='text-caption'>{working}</span> : null}
149+
</Chip>
150+
</PopoverTrigger>
151+
<PopoverContent align='end' className='w-[340px] max-w-[calc(100vw-32px)] p-0'>
152+
<div className='max-h-[60vh] overflow-y-auto'>{details}</div>
153+
</PopoverContent>
154+
</Popover>
155+
</header>
156+
)
157+
}

‎apps/sim/app/o/[organizationId]/home/organization-home.tsx‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
} from '@/lib/mothership/events'
2323
import { createSearchResource } from '@/lib/mothership/resources/search'
2424
import { OrganizationLanding } from '@/app/o/[organizationId]/components/organization-landing'
25+
import { ChatDetails } from '@/app/o/[organizationId]/home/components/chat-details'
2526
import { Composer } from '@/app/o/[organizationId]/home/components/composer'
2627
import { GetStarted } from '@/app/o/[organizationId]/home/components/get-started'
2728
import { projectPaneParsers } from '@/app/o/[organizationId]/home/components/project-pane/search-params'
@@ -477,14 +478,21 @@ function OrganizationHomeContent({
477478
const hostLanding = hasChat ? undefined : landing?.(requestMode)
478479
const content = (
479480
<div className='flex h-full min-h-0 min-w-0 flex-1 flex-col bg-[var(--bg)]'>
480-
{projectViewEnabled && (
481+
{projectViewEnabled && !lookupMode ? (
482+
<ChatDetails
483+
key={chat.resolvedChatId ?? chatId ?? 'new'}
484+
title={chatTitle || 'New chat'}
485+
messages={chat.messages}
486+
isSending={chat.isSending || chat.isReconnecting}
487+
/>
488+
) : projectViewEnabled ? (
481489
<header className='flex h-[calc(var(--resource-header-controls-height)+1px)] shrink-0 items-center gap-2 border-[var(--border)] border-b px-4'>
482490
<OverflowText
483491
label={chatTitle || (lookupMode ? 'Search' : 'New chat')}
484492
className='text-[var(--text-body)] text-small'
485493
/>
486494
</header>
487-
)}
495+
) : null}
488496
{lookupMode ? (
489497
<SearchResultsView
490498
embedded

0 commit comments

Comments
 (0)