Skip to content

Commit ceba8cf

Browse files
committed
improvement(ui/ux): mothership chat experience
1 parent 6accb38 commit ceba8cf

File tree

25 files changed

+469
-259
lines changed

25 files changed

+469
-259
lines changed

apps/sim/app/(home)/components/features/components/features-preview.tsx

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,27 @@ const SCATTERED_ICONS: IconEntry[] = [
3131
{ key: 'anthropic', icon: AnthropicIcon, label: 'Anthropic', top: '10%', left: '78%' },
3232
{ key: 'gmail', icon: GmailIcon, label: 'Gmail', top: '24%', left: '90%' },
3333
{ key: 'salesforce', icon: SalesforceIcon, label: 'Salesforce', top: '28%', left: '6%' },
34-
{ key: 'table', icon: Table, label: 'Tables', top: '22%', left: '30%' },
34+
{
35+
key: 'table',
36+
icon: Table,
37+
label: 'Tables',
38+
top: '22%',
39+
left: '30%',
40+
color: 'var(--text-icon)',
41+
},
3542
{ key: 'xai', icon: xAIIcon, label: 'xAI', top: '26%', left: '66%' },
3643
{ key: 'hubspot', icon: HubspotIcon, label: 'HubSpot', top: '55%', left: '4%', color: '#FF7A59' },
37-
{ key: 'database', icon: Database, label: 'Database', top: '74%', left: '68%' },
38-
{ key: 'file', icon: File, label: 'Files', top: '70%', left: '18%' },
44+
{
45+
key: 'database',
46+
icon: Database,
47+
label: 'Database',
48+
top: '74%',
49+
left: '68%',
50+
color: 'var(--text-icon)',
51+
},
52+
{ key: 'file', icon: File, label: 'Files', top: '70%', left: '18%', color: 'var(--text-icon)' },
3953
{ key: 'gemini', icon: GeminiIcon, label: 'Gemini', top: '58%', left: '86%' },
40-
{ key: 'logs', icon: Library, label: 'Logs', top: '86%', left: '44%' },
54+
{ key: 'logs', icon: Library, label: 'Logs', top: '86%', left: '44%', color: 'var(--text-icon)' },
4155
{ key: 'groq', icon: GroqIcon, label: 'Groq', top: '90%', left: '82%' },
4256
]
4357

apps/sim/app/(home)/components/navbar/navbar.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ const NAV_LINKS: NavLink[] = [
1919
]
2020

2121
/** Logo and nav edge: horizontal padding (px) for left/right symmetry. */
22-
const LOGO_CELL = 'flex items-center px-[20px]'
22+
const LOGO_CELL = 'flex items-center pl-[80px] pr-[20px]'
2323

2424
/** Links: even spacing between items. */
2525
const LINK_CELL = 'flex items-center px-[14px]'
@@ -97,7 +97,7 @@ export default function Navbar({ logoOnly = false }: NavbarProps) {
9797
<div className='flex-1' />
9898

9999
{/* CTAs */}
100-
<div className='flex items-center gap-[8px] px-[20px]'>
100+
<div className='flex items-center gap-[8px] pr-[80px] pl-[20px]'>
101101
<Link
102102
href='/login'
103103
className='inline-flex h-[30px] items-center rounded-[5px] border border-[#3d3d3d] px-[9px] text-[#ECECEC] text-[13.5px] transition-colors hover:bg-[#2A2A2A]'

apps/sim/app/(home)/components/pricing/pricing.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ function PricingCard({ tier }: PricingCardProps) {
174174
export default function Pricing() {
175175
return (
176176
<section id='pricing' aria-labelledby='pricing-heading' className='bg-[#F6F6F6]'>
177-
<div className='px-4 pt-[100px] pb-[80px] sm:px-8 md:px-[80px]'>
177+
<div className='px-4 pt-[80px] pb-[160px] sm:px-8 md:px-[80px]'>
178178
<div className='flex flex-col items-start gap-3 sm:gap-4 md:gap-[20px]'>
179179
<Badge
180180
variant='blue'

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

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ export function AgentGroup({
5858
}, [expanded])
5959

6060
return (
61-
<div className='flex flex-col gap-1.5'>
61+
<div className='flex flex-col gap-[6px]'>
6262
{hasItems ? (
6363
<button
6464
type='button'
@@ -87,7 +87,7 @@ export function AgentGroup({
8787
{hasItems && mounted && (
8888
<div
8989
className={cn(
90-
'flex flex-col gap-3 transition-opacity duration-300 ease-out',
90+
'flex flex-col gap-[6px] transition-opacity duration-300 ease-out',
9191
expanded ? 'opacity-100' : 'opacity-0'
9292
)}
9393
>
@@ -98,14 +98,15 @@ export function AgentGroup({
9898
toolName={item.data.toolName}
9999
displayTitle={item.data.displayTitle}
100100
status={item.data.status}
101+
result={item.data.result}
101102
/>
102103
) : (
103-
<div
104+
<span
104105
key={`text-${idx}`}
105-
className='ml-[24px] rounded-lg border border-[var(--divider)] bg-[var(--surface-4)] px-3 py-2 text-[13px] text-[var(--text-tertiary)] italic'
106+
className='pl-[24px] font-base text-[13px] text-[var(--text-secondary)]'
106107
>
107108
{item.content.trim()}
108-
</div>
109+
</span>
109110
)
110111
)}
111112
</div>

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

Lines changed: 107 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,29 @@
1-
import { Loader } from '@/components/emcn'
2-
import type { ToolCallStatus } from '../../../../types'
1+
'use client'
2+
3+
import { useMemo } from 'react'
4+
import { PillsRing } from '@/components/emcn'
5+
import type { ToolCallResult, ToolCallStatus } from '../../../../types'
36
import { getToolIcon } from '../../utils'
47

8+
/** Tools that render as cards with result data on success. */
9+
const CARD_TOOLS = new Set<string>([
10+
'function_execute',
11+
'search_online',
12+
'scrape_page',
13+
'get_page_contents',
14+
'search_library_docs',
15+
'superagent',
16+
'run',
17+
'plan',
18+
'debug',
19+
'edit',
20+
'fast_edit',
21+
'custom_tool',
22+
'research',
23+
'agent',
24+
'job',
25+
])
26+
527
function CircleCheck({ className }: { className?: string }) {
628
return (
729
<svg
@@ -40,29 +62,100 @@ export function CircleStop({ className }: { className?: string }) {
4062
)
4163
}
4264

65+
function StatusIcon({ status, toolName }: { status: ToolCallStatus; toolName: string }) {
66+
if (status === 'executing') {
67+
return <PillsRing className='h-[15px] w-[15px] text-[var(--text-tertiary)]' animate />
68+
}
69+
if (status === 'cancelled') {
70+
return <CircleStop className='h-[15px] w-[15px] text-[var(--text-tertiary)]' />
71+
}
72+
const Icon = getToolIcon(toolName)
73+
if (Icon) {
74+
return <Icon className='h-[15px] w-[15px] text-[var(--text-tertiary)]' />
75+
}
76+
return <CircleCheck className='h-[15px] w-[15px] text-[var(--text-tertiary)]' />
77+
}
78+
79+
function FlatToolLine({
80+
toolName,
81+
displayTitle,
82+
status,
83+
}: {
84+
toolName: string
85+
displayTitle: string
86+
status: ToolCallStatus
87+
}) {
88+
return (
89+
<div className='flex items-center gap-[8px] pl-[24px]'>
90+
<div className='flex h-[16px] w-[16px] flex-shrink-0 items-center justify-center'>
91+
<StatusIcon status={status} toolName={toolName} />
92+
</div>
93+
<span className='font-base text-[13px] text-[var(--text-secondary)]'>{displayTitle}</span>
94+
</div>
95+
)
96+
}
97+
98+
function formatToolOutput(output: unknown): string {
99+
if (output === null || output === undefined) return ''
100+
if (typeof output === 'string') return output
101+
try {
102+
return JSON.stringify(output, null, 2)
103+
} catch {
104+
return String(output)
105+
}
106+
}
107+
43108
interface ToolCallItemProps {
44109
toolName: string
45110
displayTitle: string
46111
status: ToolCallStatus
112+
result?: ToolCallResult
113+
}
114+
115+
export function ToolCallItem({ toolName, displayTitle, status, result }: ToolCallItemProps) {
116+
const showCard =
117+
CARD_TOOLS.has(toolName) &&
118+
status === 'success' &&
119+
result?.output !== undefined &&
120+
result?.output !== null
121+
122+
if (showCard) {
123+
return <ToolCallCard toolName={toolName} displayTitle={displayTitle} result={result!} />
124+
}
125+
126+
return <FlatToolLine toolName={toolName} displayTitle={displayTitle} status={status} />
47127
}
48128

49-
export function ToolCallItem({ toolName, displayTitle, status }: ToolCallItemProps) {
129+
function ToolCallCard({
130+
toolName,
131+
displayTitle,
132+
result,
133+
}: {
134+
toolName: string
135+
displayTitle: string
136+
result: ToolCallResult
137+
}) {
138+
const body = useMemo(() => formatToolOutput(result.output), [result.output])
50139
const Icon = getToolIcon(toolName)
140+
const ResolvedIcon = Icon ?? CircleCheck
51141

52142
return (
53-
<div className='flex items-center gap-[8px] pl-[24px]'>
54-
<div className='flex h-[16px] w-[16px] flex-shrink-0 items-center justify-center'>
55-
{status === 'executing' ? (
56-
<Loader className='h-[15px] w-[15px] text-[var(--text-tertiary)]' animate />
57-
) : status === 'cancelled' ? (
58-
<CircleStop className='h-[15px] w-[15px] text-[var(--text-tertiary)]' />
59-
) : Icon ? (
60-
<Icon className='h-[15px] w-[15px] text-[var(--text-tertiary)]' />
61-
) : (
62-
<CircleCheck className='h-[15px] w-[15px] text-[var(--text-tertiary)]' />
143+
<div className='animate-stream-fade-in pl-[24px]'>
144+
<div className='overflow-hidden rounded-[8px] border border-[var(--border)] bg-[var(--surface-3)]'>
145+
<div className='flex items-center gap-[8px] px-[10px] py-[6px]'>
146+
<div className='flex h-[16px] w-[16px] flex-shrink-0 items-center justify-center'>
147+
<ResolvedIcon className='h-[15px] w-[15px] text-[var(--text-tertiary)]' />
148+
</div>
149+
<span className='font-base text-[13px] text-[var(--text-secondary)]'>{displayTitle}</span>
150+
</div>
151+
{body && (
152+
<div className='border-[var(--border)] border-t px-[10px] py-[6px]'>
153+
<pre className='max-h-[200px] overflow-y-auto whitespace-pre-wrap break-all font-mono text-[12px] text-[var(--text-body)] leading-[1.5]'>
154+
{body}
155+
</pre>
156+
</div>
63157
)}
64158
</div>
65-
<span className='font-base text-[13px] text-[var(--text-secondary)]'>{displayTitle}</span>
66159
</div>
67160
)
68161
}

apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-content.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -216,9 +216,9 @@ export function ChatContent({ content, isStreaming = false, onOptionSelect }: Ch
216216
return (
217217
<div className='space-y-3'>
218218
{parsed.segments.map((segment, i) => {
219-
if (segment.type === 'text') {
219+
if (segment.type === 'text' || segment.type === 'thinking') {
220220
return (
221-
<div key={`text-${i}`} className={PROSE_CLASSES}>
221+
<div key={`${segment.type}-${i}`} className={PROSE_CLASSES}>
222222
<ReactMarkdown remarkPlugins={REMARK_PLUGINS} components={MARKDOWN_COMPONENTS}>
223223
{segment.content}
224224
</ReactMarkdown>

apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -144,13 +144,8 @@ interface SpecialTagsProps {
144144
*/
145145
export function SpecialTags({ segment, onOptionSelect }: SpecialTagsProps) {
146146
switch (segment.type) {
147-
/** TODO: FIX THINKING BLOCK RENDERING*/
148147
case 'thinking':
149-
return (
150-
<div className='rounded-lg border border-[var(--divider)] bg-[var(--surface-4)] px-3 py-2 text-[13px] text-[var(--text-tertiary)] italic'>
151-
{segment.content.trim()}
152-
</div>
153-
)
148+
return null
154149
case 'options':
155150
return <OptionsDisplay data={segment.data} onSelect={onOptionSelect} />
156151
case 'usage_upgrade':

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

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import type { ContentBlock, OptionItem, SubagentName, ToolCallData } from '../../types'
44
import { SUBAGENT_LABELS } from '../../types'
55
import type { AgentGroupItem } from './components'
6-
import { AgentGroup, ChatContent, CircleStop, Options } from './components'
6+
import { AgentGroup, ChatContent, CircleStop, Options, PendingTagIndicator } from './components'
77

88
interface TextSegment {
99
type: 'text'
@@ -49,6 +49,7 @@ function toToolData(tc: NonNullable<ContentBlock['toolCall']>): ToolCallData {
4949
toolName: tc.name,
5050
displayTitle: tc.displayTitle || formatToolName(tc.name),
5151
status: tc.status,
52+
result: tc.result,
5253
}
5354
}
5455

@@ -186,6 +187,14 @@ function parseBlocks(blocks: ContentBlock[]): MessageSegment[] {
186187
continue
187188
}
188189

190+
if (block.type === 'subagent_end') {
191+
if (group) {
192+
segments.push(group)
193+
group = null
194+
}
195+
continue
196+
}
197+
189198
if (block.type === 'stopped') {
190199
if (group) {
191200
segments.push(group)
@@ -223,6 +232,27 @@ export function MessageContent({
223232

224233
if (segments.length === 0) return null
225234

235+
const lastSegment = segments[segments.length - 1]
236+
const hasTrailingContent = lastSegment.type === 'text' || lastSegment.type === 'stopped'
237+
238+
let allLastGroupToolsDone = false
239+
if (lastSegment.type === 'agent_group') {
240+
const toolItems = lastSegment.items.filter((item) => item.type === 'tool')
241+
allLastGroupToolsDone =
242+
toolItems.length > 0 &&
243+
toolItems.every(
244+
(t) =>
245+
t.type === 'tool' &&
246+
(t.data.status === 'success' ||
247+
t.data.status === 'error' ||
248+
t.data.status === 'cancelled')
249+
)
250+
}
251+
252+
const hasSubagentEnded = blocks.some((b) => b.type === 'subagent_end')
253+
const showTrailingThinking =
254+
isStreaming && !hasTrailingContent && (hasSubagentEnded || allLastGroupToolsDone)
255+
226256
return (
227257
<div className='space-y-[10px]'>
228258
{segments.map((segment, i) => {
@@ -279,6 +309,11 @@ export function MessageContent({
279309
)
280310
}
281311
})}
312+
{showTrailingThinking && (
313+
<div className='animate-stream-fade-in-delayed opacity-0'>
314+
<PendingTagIndicator />
315+
</div>
316+
)}
282317
</div>
283318
)
284319
}

apps/sim/app/workspace/[workspaceId]/home/components/message-content/utils.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import type { ComponentType, SVGProps } from 'react'
22
import {
33
Asterisk,
44
Blimp,
5-
BubbleChatPreview,
65
Bug,
76
Calendar,
87
ClipboardList,
@@ -23,6 +22,7 @@ import {
2322
Wrench,
2423
} from '@/components/emcn'
2524
import { Table as TableIcon } from '@/components/emcn/icons'
25+
import { AgentIcon } from '@/components/icons'
2626
import type { MothershipToolName, SubagentName } from '../../types'
2727

2828
export type IconComponent = ComponentType<SVGProps<SVGSVGElement>>
@@ -53,7 +53,7 @@ const TOOL_ICONS: Record<MothershipToolName | SubagentName | 'mothership', IconC
5353
knowledge_base: Database,
5454
table: TableIcon,
5555
job: Calendar,
56-
agent: BubbleChatPreview,
56+
agent: AgentIcon,
5757
custom_tool: Wrench,
5858
research: Search,
5959
plan: ClipboardList,

apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/mothership-view.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ export const MothershipView = memo(function MothershipView({
5959
return (
6060
<div
6161
className={cn(
62-
'flex h-full flex-col overflow-hidden border-[var(--border)] transition-[width,min-width,border-width] duration-300 ease-out',
62+
'relative z-10 flex h-full flex-col overflow-hidden border-[var(--border)] bg-[var(--bg)] transition-[width,min-width,border-width] duration-300 ease-out',
6363
isCollapsed ? 'w-0 min-w-0 border-l-0' : 'w-[50%] min-w-[400px] border-l',
6464
className
6565
)}

0 commit comments

Comments
 (0)