Skip to content

Commit 34ba9e7

Browse files
andresdjassoclaude
andcommitted
feat(emcn): ThinkingLoader — the Recents icon spins while the chat generates
New emcn thinking-loader component wired through the chat title bar and switcher: while a response streams, the switcher's bubble-clock icon becomes a spinner so work-in-progress reads even with the messages off screen. Also refreshes the paperclip and slash icons, simplifies the pending-tag indicator to use the shared loader, trims dead tailwind config, and adds a playground demo. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 4519b6c commit 34ba9e7

12 files changed

Lines changed: 563 additions & 32 deletions

File tree

apps/sim/app/playground/page.tsx

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ import {
7575
TagInput,
7676
type TagItem,
7777
Textarea,
78+
ThinkingLoader,
7879
TimePicker,
7980
ToastProvider,
8081
Tooltip,
@@ -1007,6 +1008,39 @@ export default function PlaygroundPage() {
10071008
</VariantRow>
10081009
</Section>
10091010

1011+
{/* ThinkingLoader */}
1012+
<Section title='ThinkingLoader'>
1013+
<VariantRow label='cycle (default)'>
1014+
<div className='flex items-center gap-6'>
1015+
<ThinkingLoader />
1016+
<ThinkingLoader size={28} />
1017+
<ThinkingLoader size={40} />
1018+
<ThinkingLoader label='Thinking…' />
1019+
</div>
1020+
</VariantRow>
1021+
{(
1022+
[
1023+
'metaballs',
1024+
'orbit',
1025+
'relay',
1026+
'corners',
1027+
'burst',
1028+
'compass',
1029+
'squeeze',
1030+
'maze',
1031+
] as const
1032+
).map((variant) => (
1033+
<VariantRow key={variant} label={variant}>
1034+
<div className='flex items-center gap-6'>
1035+
<ThinkingLoader variant={variant} />
1036+
<ThinkingLoader variant={variant} size={28} />
1037+
<ThinkingLoader variant={variant} size={40} />
1038+
<ThinkingLoader variant={variant} label='Thinking…' />
1039+
</div>
1040+
</VariantRow>
1041+
))}
1042+
</Section>
1043+
10101044
{/* Icons */}
10111045
<Section title='Icons'>
10121046
<div className='grid grid-cols-6 gap-4 sm:grid-cols-8 md:grid-cols-10'>

apps/sim/app/workspace/[workspaceId]/components/chat-switcher/chat-switcher.tsx

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
Popover,
88
PopoverAnchor,
99
PopoverContent,
10+
ThinkingLoader,
1011
Tooltip,
1112
} from '@/components/emcn'
1213
import { BubbleChatDelay, ChevronDown } from '@/components/emcn/icons'
@@ -64,6 +65,12 @@ interface ChatSwitcherProps {
6465
* to reopen a hidden chat pane (including re-picking the current chat).
6566
*/
6667
onSelectChat?: (chatId: string) => void
68+
/**
69+
* The chat is generating a response — the recents icon becomes a spinner so
70+
* the title bar signals work in progress even when the messages are off
71+
* screen (collapsed pane, scrolled away).
72+
*/
73+
isWorking?: boolean
6774
}
6875

6976
/**
@@ -76,6 +83,7 @@ export function ChatSwitcher({
7683
isNewChat = false,
7784
iconOnly = false,
7885
onSelectChat,
86+
isWorking = false,
7987
}: ChatSwitcherProps) {
8088
const isHidden = useSidebarToggleHidden()
8189
const { workspaceId } = useParams<{ workspaceId?: string }>()
@@ -117,6 +125,12 @@ export function ChatSwitcher({
117125
router.push(`/workspace/${workspaceId}/chat/${selectedChatId}`)
118126
}
119127

128+
const chipIcon = isWorking ? (
129+
<ThinkingLoader size={18} className='flex-shrink-0' />
130+
) : (
131+
<BubbleChatDelay className='size-[16px] flex-shrink-0 text-[var(--text-icon)]' />
132+
)
133+
120134
const trigger = iconOnly ? (
121135
<button
122136
type='button'
@@ -128,7 +142,7 @@ export function ChatSwitcher({
128142
open && 'bg-[var(--surface-active)]'
129143
)}
130144
>
131-
<BubbleChatDelay className='size-[16px] flex-shrink-0 text-[var(--text-icon)]' />
145+
{chipIcon}
132146
<ChevronDown className='size-[14px] flex-shrink-0 text-[var(--text-icon)]' />
133147
</button>
134148
) : (
@@ -142,7 +156,7 @@ export function ChatSwitcher({
142156
open && 'bg-[var(--surface-active)]'
143157
)}
144158
>
145-
<BubbleChatDelay className='size-[16px] flex-shrink-0 text-[var(--text-icon)]' />
159+
{chipIcon}
146160
<span className='min-w-0 truncate font-medium text-[14px] text-[var(--text-primary)]'>
147161
{title}
148162
</span>

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

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
Expandable,
99
ExpandableContent,
1010
SecretReveal,
11+
ThinkingLoader,
1112
} from '@/components/emcn'
1213
import { canonicalWorkspaceFilePath } from '@/lib/copilot/vfs/path-utils'
1314
import { cn } from '@/lib/core/utils/cn'
@@ -356,13 +357,6 @@ export function parseSpecialTags(content: string, isStreaming: boolean): ParsedS
356357
return { segments, hasPendingTag }
357358
}
358359

359-
const THINKING_BLOCKS = [
360-
{ color: '#2ABBF8', delay: '0s' },
361-
{ color: '#00F701', delay: '0.2s' },
362-
{ color: '#FA4EDF', delay: '0.6s' },
363-
{ color: '#FFCC02', delay: '0.4s' },
364-
] as const
365-
366360
interface SpecialTagsProps {
367361
segment: Exclude<ContentSegment, { type: 'text' }>
368362
onOptionSelect?: (id: string) => void
@@ -401,17 +395,8 @@ export function SpecialTags({
401395
*/
402396
export function PendingTagIndicator() {
403397
return (
404-
<div className='flex animate-stream-fade-in items-center gap-2 py-2'>
405-
<div className='grid size-[16px] grid-cols-2 gap-[1.5px]'>
406-
{THINKING_BLOCKS.map((block, i) => (
407-
<div
408-
key={i}
409-
className='animate-thinking-block rounded-xs'
410-
style={{ backgroundColor: block.color, animationDelay: block.delay }}
411-
/>
412-
))}
413-
</div>
414-
<span className='text-[var(--text-body)] text-sm'>Thinking…</span>
398+
<div className='flex animate-stream-fade-in items-center py-2'>
399+
<ThinkingLoader label='Thinking…' />
415400
</div>
416401
)
417402
}

apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/components/chat-title-bar/chat-title-bar.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ interface ChatTitleBarProps {
1515
onSelectChat?: (chatId: string) => void
1616
/** Renders a close (×) control at the bar's right edge that hides the chat pane. */
1717
onClose?: () => void
18+
/** The chat is generating a response — the switcher's recents icon becomes a spinner. */
19+
isWorking?: boolean
1820
}
1921

2022
/**
@@ -23,15 +25,20 @@ interface ChatTitleBarProps {
2325
* straight between chats without returning to the new-chat view. Selecting a
2426
* chat navigates to it.
2527
*/
26-
export function ChatTitleBar({ chatId, onSelectChat, onClose }: ChatTitleBarProps) {
28+
export function ChatTitleBar({ chatId, onSelectChat, onClose, isWorking }: ChatTitleBarProps) {
2729
return (
2830
<div className='flex h-[44px] flex-shrink-0 items-center gap-1 border-[var(--border)] border-b px-4'>
2931
{/* Edge controls pull out by 9px so their 30px hover pills sit 7px from
3032
the panel edge — matching the pill's 7px top/bottom gap in the bar. */}
3133
<SidebarToggle className='-ml-[9px]' />
3234
{/* The title bar only renders on chat surfaces, so no chat id means the
3335
new-chat empty state — never fall back to the most recent chat. */}
34-
<ChatSwitcher chatId={chatId} isNewChat={!chatId} onSelectChat={onSelectChat} />
36+
<ChatSwitcher
37+
chatId={chatId}
38+
isNewChat={!chatId}
39+
onSelectChat={onSelectChat}
40+
isWorking={isWorking}
41+
/>
3542
{onClose && (
3643
<Tooltip.Root>
3744
<Tooltip.Trigger asChild>

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -308,7 +308,9 @@ export function MothershipChat({
308308
onWorkspaceResourceSelect={onWorkspaceResourceSelect}
309309
>
310310
<div className={cn('flex h-full min-h-0 flex-col', className)}>
311-
{layout === 'mothership-view' && <ChatTitleBar chatId={chatId} onClose={onCloseChat} />}
311+
{layout === 'mothership-view' && (
312+
<ChatTitleBar chatId={chatId} onClose={onCloseChat} isWorking={isStreamActive} />
313+
)}
312314
<div className='relative flex min-h-0 flex-1 flex-col'>
313315
<div ref={setScrollContainer} className={cn(styles.scrollContainer, SCROLLBAR_AUTOHIDE)}>
314316
{isLoading && !hasMessages ? (

apps/sim/app/workspace/[workspaceId]/home/home.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -628,7 +628,11 @@ export function Home({ chatId, userName, userId, initialResourceId = null }: Hom
628628
the toggle and switcher never shift when the pane closes. */
629629
<div className='flex flex-shrink-0 items-center gap-1'>
630630
<SidebarToggle className='-ml-[9px]' />
631-
<ChatSwitcher chatId={activeChatId} onSelectChat={reopenChatPane} />
631+
<ChatSwitcher
632+
chatId={activeChatId}
633+
onSelectChat={reopenChatPane}
634+
isWorking={isSending || isReconnecting}
635+
/>
632636
</div>
633637
) : undefined
634638
}

apps/sim/components/emcn/components/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,11 @@ export {
155155
} from './table/table'
156156
export { type FileInputOptions, TagInput, type TagItem } from './tag-input/tag-input'
157157
export { Textarea } from './textarea/textarea'
158+
export {
159+
ThinkingLoader,
160+
type ThinkingLoaderProps,
161+
type ThinkingLoaderVariant,
162+
} from './thinking-loader/thinking-loader'
158163
export { TimePicker, timePickerVariants } from './time-picker/time-picker'
159164
export { ToastProvider, toast, useToast } from './toast/toast'
160165
export {

0 commit comments

Comments
 (0)