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
@@ -1,14 +1,20 @@
import { Table } from '@sim/emcn/icons'
import { SlackIcon } from '@/components/icons'
import { ActivityStatus } from '@/components/ui/activity-status'
import { getToolStatusDisplayTitle } from '@/lib/mothership/tools/tool-display'
import {
getToolInProgressTitle,
getToolStatusDisplayTitle,
} from '@/lib/mothership/tools/tool-display'
import type {
ToolActivityPresentation,
ToolCallItemProps,
} from '@/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-call-item'
import { getToolIcon } from '@/app/workspace/[workspaceId]/home/components/message-content/utils'

/** Demo fixtures have known brands, so the landing page never loads the block registry. */
/**
* Demo fixtures have known brands, so the landing page never loads the block registry.
* Rows are history and never shimmer; the lane decides which header is live.
*/
export function HeroToolCallItem({
toolCallId,
renderStatus,
Expand All @@ -25,13 +31,8 @@ export function HeroToolCallItem({
: getToolIcon(toolName)
const activity: ToolActivityPresentation = {
label: getToolStatusDisplayTitle(displayTitle, status, toolName, activityDescription),
activeLabel: getToolStatusDisplayTitle(
displayTitle,
status === 'success' ? 'executing' : status,
toolName,
activityDescription
),
isActive: status === 'executing',
activeLabel: getToolInProgressTitle(displayTitle, status, toolName, activityDescription),
isActive: false,
icon: <Icon className='size-full' />,
}
return renderStatus ? renderStatus(activity) : <ActivityStatus {...activity} />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
/**
* @vitest-environment jsdom
*/
import { act } from 'react'
import { createRoot, type Root } from 'react-dom/client'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { AgentGroup } from '@/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group'
import type { AgentGroupItem } from '@/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group-view'
import type { ToolCallStatus } from '@/app/workspace/[workspaceId]/home/types'

vi.mock('@/lib/browser-agent/transport', () => ({ isBrowserAgentAvailable: () => true }))

const ICON_SLOT = 'size-[14px]'

function tool(
id: string,
toolName = 'search_docs',
status: ToolCallStatus = 'success'
): AgentGroupItem {
return { type: 'tool', data: { id, toolName, displayTitle: `Searching ${id}`, status } }
}

function lane(id: string, items: AgentGroupItem[]): AgentGroupItem {
return {
type: 'agent_group',
group: {
id,
agentName: 'deploy',
agentLabel: 'Deploy',
items,
isDelegating: false,
isOpen: false,
},
}
}

/** Indentation classes a row or its containers must not carry. */
const hasIndent = (element: Element) =>
[...element.classList].some((name) => /^(pl|ml|ps|ms)-/.test(name))

describe('flat expanded activity layout', () => {
let root: Root
let container: HTMLDivElement

beforeEach(() => {
vi.stubGlobal('matchMedia', vi.fn().mockReturnValue({ matches: false }))
;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true
container = document.createElement('div')
document.body.appendChild(container)
root = createRoot(container)
})

afterEach(() => {
act(() => root.unmount())
container.remove()
vi.unstubAllGlobals()
})

const render = (agentName: string, items: AgentGroupItem[], error?: string) =>
act(() =>
root.render(
<AgentGroup agentName={agentName} agentLabel='Agent' items={items} error={error} />
)
)
const expand = () => act(() => container.querySelector<HTMLElement>('[role="button"]')!.click())
const statuses = () => [...container.querySelectorAll<HTMLElement>('[role="status"]')]
const iconSlot = (status: Element) => status.querySelector(':scope > [aria-hidden="true"]')!
/** The reserved icon slot of a text-column wrapper. */
const reservedSlot = (column: Element) => column.querySelector(':scope > [aria-hidden="true"]')

it.each(['mothership', 'workflow'])(
'lines %s rows up with the header icon and text columns',
(agentName) => {
render(agentName, [tool('a'), tool('b', 'web_search')])
expand()
const [header, ...rows] = statuses()
expect(rows.length).toBeGreaterThan(0)
for (const row of [header, ...rows]) {
expect(row.classList).toContain('gap-2')
expect(iconSlot(row).classList).toContain(ICON_SLOT)
}
const list = rows[0].closest('.flex-col')!
expect(list.classList).toContain('gap-1.5')
let node: Element | null = rows[0]
while (node && node !== container) {
expect(hasIndent(node), node.className).toBe(false)
node = node.parentElement
}
}
)

it('keeps a nested lane indented into its parent text column', () => {
render('workflow', [tool('a'), lane('deploy', [tool('child')])])
expand()
const nestedHeader = statuses().find((status) => status.textContent === 'Searched child')!
const column = nestedHeader.closest('.items-start')!
expect(column.classList).toContain('gap-2')
expect(reservedSlot(column)?.classList).toContain(ICON_SLOT)
expect(reservedSlot(column)?.childElementCount).toBe(0)
})

it('puts a lane error on the shared text column instead of a hand-tuned inset', () => {
render('workflow', [tool('a')], 'Subagent failed.')
const error = [...container.querySelectorAll('p')].find(
(node) => node.textContent === 'Subagent failed.'
)!
expect(hasIndent(error)).toBe(false)
const column = error.closest('.items-start')!
expect(column.classList).toContain('gap-2')
expect(reservedSlot(column)?.classList).toContain(ICON_SLOT)
})

it('stacks main-lane blocks one gap-3 apart and search queries one gap-1.5 apart', () => {
render('mothership', [
tool('a'),
{
type: 'tool',
data: {
id: 's1',
toolName: 'search_workspace',
displayTitle: 'Searching',
status: 'success',
params: { query: 'first' },
},
},
{
type: 'tool',
data: {
id: 's2',
toolName: 'search_workspace',
displayTitle: 'Searching',
status: 'success',
params: { query: 'second' },
},
},
])
const blocks = container.querySelector('.flex-col.gap-3')!
expect(blocks.contains(statuses()[0])).toBe(true)
expect(blocks.classList).toContain('gap-3')
expect(blocks.classList).not.toContain('gap-1.5')
const queries = statuses().filter((status) => status.textContent === 'first')
const searchList = queries[0].closest('.flex-col.gap-1\\.5')!
expect(searchList).not.toBeNull()
expect(searchList.parentElement?.closest('.flex-col.gap-3')).toBe(blocks)
})
})
Loading
Loading