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 @@ -217,7 +217,6 @@ export function HeroChatLoop({
agentLabel='Workflow Agent'
items={WORKFLOW_AGENT_BUILDING_ITEMS}
isStreaming
isCurrentSection
isLaneOpen
defaultExpanded
autoScrollActivity={false}
Expand All @@ -237,7 +236,6 @@ export function HeroChatLoop({
agentName='mothership'
agentLabel='Sim'
items={SIM_ITEMS}
defaultExpanded
/>
<HeroChatReply
content={replyComplete ? replyMessage : replyWords.slice(0, revealedWords).join('')}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Table } from '@sim/emcn/icons'
import { SlackIcon } from '@/components/icons'
import { ActivityStatus } from '@/components/ui/activity-status'
import { getToolStatusDisplayTitle } from '@/lib/copilot/tools/tool-display'
import type { ToolCallItemProps } from '@/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-call-item'
import { ToolCallRow } from '@/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-call-row'

/** Demo fixtures have known brands, so the landing page never loads the block registry. */
export function HeroToolCallItem({
Expand All @@ -18,9 +18,9 @@ export function HeroToolCallItem({
? Table
: undefined
return (
<ToolCallRow
title={getToolStatusDisplayTitle(displayTitle, status, toolName)}
isExecuting={status === 'executing'}
<ActivityStatus
label={getToolStatusDisplayTitle(displayTitle, status, toolName)}
isActive={status === 'executing'}
icon={Icon && <Icon className='size-[14px] shrink-0 text-[var(--text-icon)]' />}
/>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { useMemo } from 'react'
import { Chip, ChipLink } from '@sim/emcn'
import { useQueryStates } from 'nuqs'
import { ShimmerText } from '@/components/ui/shimmer-text'
import type {
WorkspaceKnowledgeSearchResult,
WorkspaceSearchFilters,
Expand Down Expand Up @@ -188,7 +189,11 @@ export function KnowledgeSearchResults({
)
}
if (isPending || (isFetching && !results)) {
return <p className='px-2 py-2 text-[var(--text-muted)] text-caption'>Searching…</p>
return (
<p role='status' className='px-2 py-2 text-[var(--text-muted)] text-caption'>
<ShimmerText className='[--shimmer-rest:var(--text-muted)]'>Searching…</ShimmerText>
</p>
)
}

const indexingNote =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { ShimmerText } from '@/components/ui'
import { isBrowserAgentAvailable } from '@/lib/browser-agent/transport'
import { RETIRED_BROWSER_REQUEST_TAKEOVER_ID } from '@/lib/copilot/tools/retired-tools'
import { renderInlineMarkdown } from '@/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/inline-markdown'
import { getVisibleMainAgentItems } from '@/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/main-agent-activity'
import type { ToolCallItemProps } from '@/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-call-item'
import {
getAgentIcon,
Expand Down Expand Up @@ -47,11 +48,9 @@ export interface AgentGroupProps {
items: AgentGroupItem[]
isDelegating?: boolean
isStreaming?: boolean
/** This group is the latest section in its parent sequence (drives collapse). */
isCurrentSection?: boolean
/** The subagent lane is still open (no subagent_end yet) — i.e. actively running. */
isLaneOpen?: boolean
/** Opens the group on first render without changing production's automatic collapse rules. */
/** Opens a subagent group on first render. */
defaultExpanded?: boolean
/** Keeps the activity viewport anchored at the top while new rows stream in. */
autoScrollActivity?: boolean
Expand Down Expand Up @@ -151,7 +150,6 @@ export function AgentGroupView({
items,
isDelegating = false,
isStreaming = false,
isCurrentSection = false,
isLaneOpen = false,
defaultExpanded = false,
autoScrollActivity = true,
Expand Down Expand Up @@ -192,26 +190,15 @@ export function AgentGroupView({
const isWorking =
!activeBrowserTakeover && ((isDelegating && !resolved) || (isStreaming && isLaneOpen))

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

const toggleExpanded = () => {
if (activeBrowserTakeover) {
Expand All @@ -221,9 +208,54 @@ export function AgentGroupView({
setManualExpanded(!expanded)
}

const visibleItems = isMainAgent ? getVisibleMainAgentItems(items) : items
const activity = (
<div className={cn('flex min-w-0 flex-col gap-1.5', !isMainAgent && 'py-0.5 pl-6')}>
{visibleItems.map((item, idx) => {
if (item.type === 'tool') {
return (
<ToolCallComponent
key={item.data.id}
toolCallId={item.data.id}
toolName={item.data.toolName}
displayTitle={item.data.displayTitle}
status={item.data.status}
params={item.data.params}
result={item.data.result}
streamingArgs={item.data.streamingArgs}
startedAt={item.data.startedAt}
/>
)
}
if (item.type === 'agent_group') {
return (
<AgentGroupView
key={item.group.id}
ToolCallComponent={ToolCallComponent}
renderBrowserTakeover={renderBrowserTakeover}
agentName={item.group.agentName}
agentLabel={item.group.agentLabel}
items={item.group.items}
isDelegating={item.group.isDelegating}
isStreaming={isStreaming}
isLaneOpen={item.group.isOpen}
/>
)
}
return (
<NarrationText
key={`text-${idx}`}
content={item.content}
isStreaming={isStreaming && idx === visibleItems.length - 1}
/>
)
})}
</div>
)

return (
<div className='flex flex-col gap-1.5'>
{hasItems ? (
{isMainAgent ? null : hasItems ? (
<button
type='button'
onClick={toggleExpanded}
Expand Down Expand Up @@ -256,60 +288,20 @@ export function AgentGroupView({
)}
</div>
)}
{hasItems && (
{isMainAgent ? (
activity
) : hasItems ? (
<Expandable expanded={expanded}>
<ExpandableContent>
<BoundedViewport
isStreaming={isStreaming && autoScrollActivity}
unbounded={nestedBrowserTakeover}
>
<div className='flex flex-col gap-1.5 py-0.5'>
{items.map((item, idx) => {
if (item.type === 'tool') {
return (
<ToolCallComponent
key={item.data.id}
toolCallId={item.data.id}
toolName={item.data.toolName}
displayTitle={item.data.displayTitle}
status={item.data.status}
params={item.data.params}
result={item.data.result}
streamingArgs={item.data.streamingArgs}
startedAt={item.data.startedAt}
/>
)
}
if (item.type === 'agent_group') {
return (
<div key={item.group.id} className='pl-6'>
<AgentGroupView
ToolCallComponent={ToolCallComponent}
renderBrowserTakeover={renderBrowserTakeover}
agentName={item.group.agentName}
agentLabel={item.group.agentLabel}
items={item.group.items}
isDelegating={item.group.isDelegating}
isStreaming={isStreaming}
isCurrentSection={idx === items.length - 1}
isLaneOpen={item.group.isOpen}
/>
</div>
)
}
return (
<NarrationText
key={`text-${idx}`}
content={item.content}
isStreaming={isStreaming && idx === items.length - 1}
/>
)
})}
</div>
{activity}
</BoundedViewport>
</ExpandableContent>
</Expandable>
)}
) : null}
{activeBrowserTakeover && (
<div key={activeBrowserTakeover.id} className='animate-stream-fade-in'>
{renderBrowserTakeover?.(activeBrowserTakeover.reason)}
Expand All @@ -334,7 +326,7 @@ function NarrationText({ content, isStreaming }: NarrationTextProps) {
const revealed = useSmoothText(content, isStreaming)

return (
<span className='pl-6 text-[13px] text-[var(--text-muted)] leading-[18px]'>
<span className='text-[13px] text-[var(--text-muted)] leading-[18px]'>
{renderInlineMarkdown(revealed.trim())}
</span>
)
Expand Down
Loading
Loading