Skip to content

Commit 3c22e1e

Browse files
authored
refactor(ui): eliminate prop drilling in editor, home, sidebar, and logs dashboard (#4950)
* refactor(ui): eliminate prop drilling in editor, home, sidebar, and logs dashboard - panel editor: activeSearchTarget was relayed untouched through SubBlock and 30+ input components (depth up to 6); now provided once via ActiveSearchTargetProvider and re-provided at tool-input's synthetic sub-block transformation points; removed the vestigial workspaceId option from SubBlockInputController/useSubBlockInput - home: ChatSurfaceProvider carries chatId/userId and stable interaction callbacks to UserInput/MessageContent/MessageActions; MothershipResourcesProvider carries the five resource operations to ResourceTabs, removing MothershipView's pure-relay props - sidebar: extended SidebarDragContext into SidebarListContext so WorkflowItem/FolderItem read selection/drag callbacks directly; moved the hidden import input up to sidebar.tsx (also fixes import no-op while the list shows a skeleton) - logs dashboard: DashboardSegmentsContext feeds StatusBar directly; WorkflowsList no longer relays segment selection state * refactor(home): sync ChatSurfaceProvider callback refs in a layout effect Mutating refs during render is unsound under concurrent rendering (render may run multiple times before commit); useLayoutEffect commits the latest callbacks before any user event can fire.
1 parent dd3705e commit 3c22e1e

66 files changed

Lines changed: 1211 additions & 1041 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
toast,
1818
} from '@/components/emcn'
1919
import { cn } from '@/lib/core/utils/cn'
20+
import { useChatSurface } from '@/app/workspace/[workspaceId]/home/components/chat-surface-context'
2021
import { useSubmitCopilotFeedback } from '@/hooks/queries/copilot-feedback'
2122
import { useForkMothershipChat } from '@/hooks/queries/mothership-chats'
2223
import { useFolderStore } from '@/stores/folders/store'
@@ -49,21 +50,20 @@ const BUTTON_CLASS =
4950

5051
interface MessageActionsProps {
5152
content: string
52-
chatId?: string
5353
userQuery?: string
5454
requestId?: string
5555
messageId?: string
5656
}
5757

5858
export const MessageActions = memo(function MessageActions({
5959
content,
60-
chatId,
6160
userQuery,
6261
requestId,
6362
messageId,
6463
}: MessageActionsProps) {
6564
const router = useRouter()
6665
const params = useParams<{ workspaceId: string }>()
66+
const { chatId } = useChatSurface()
6767
const [copied, setCopied] = useState(false)
6868
const [copiedRequestId, setCopiedRequestId] = useState(false)
6969
const [pendingFeedback, setPendingFeedback] = useState<'up' | 'down' | null>(null)
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
'use client'
2+
3+
import {
4+
createContext,
5+
type ReactNode,
6+
useCallback,
7+
useContext,
8+
useLayoutEffect,
9+
useMemo,
10+
useRef,
11+
} from 'react'
12+
import type { MothershipResource } from '@/app/workspace/[workspaceId]/home/types'
13+
import type { ChatContext } from '@/stores/panel'
14+
15+
/**
16+
* Identity and interaction callbacks shared across a Mothership chat surface
17+
* (home conversation view, home initial view, copilot panel). Carried via
18+
* context so leaf components (UserInput, MessageContent, MessageActions) can
19+
* consume them without relaying through every intermediate component.
20+
*/
21+
interface ChatSurfaceContextValue {
22+
/** Resolved id of the chat backing this surface, if one exists yet. */
23+
chatId?: string
24+
/** Id of the user interacting with this surface. */
25+
userId?: string
26+
/** Notifies the surface owner that a context chip was added to the input. */
27+
onContextAdd: (context: ChatContext) => void
28+
/** Notifies the surface owner that a context chip was removed from the input. */
29+
onContextRemove: (context: ChatContext) => void
30+
/** Opens a workspace resource referenced from rendered message content. */
31+
onWorkspaceResourceSelect: (resource: MothershipResource) => void
32+
}
33+
34+
const noop = () => {}
35+
36+
const ChatSurfaceContext = createContext<ChatSurfaceContextValue>({
37+
onContextAdd: noop,
38+
onContextRemove: noop,
39+
onWorkspaceResourceSelect: noop,
40+
})
41+
42+
interface ChatSurfaceProviderProps {
43+
chatId?: string
44+
userId?: string
45+
onContextAdd?: (context: ChatContext) => void
46+
onContextRemove?: (context: ChatContext) => void
47+
onWorkspaceResourceSelect?: (resource: MothershipResource) => void
48+
children: ReactNode
49+
}
50+
51+
/**
52+
* Provides the chat-surface identity and interaction callbacks to descendants.
53+
* Callbacks are latched in refs and exposed as stable wrappers so the memoized
54+
* context value only changes when `chatId` or `userId` change — consumers do
55+
* not re-render when a parent re-creates a handler.
56+
*/
57+
export function ChatSurfaceProvider({
58+
chatId,
59+
userId,
60+
onContextAdd,
61+
onContextRemove,
62+
onWorkspaceResourceSelect,
63+
children,
64+
}: ChatSurfaceProviderProps) {
65+
const onContextAddRef = useRef(onContextAdd)
66+
const onContextRemoveRef = useRef(onContextRemove)
67+
const onWorkspaceResourceSelectRef = useRef(onWorkspaceResourceSelect)
68+
69+
useLayoutEffect(() => {
70+
onContextAddRef.current = onContextAdd
71+
onContextRemoveRef.current = onContextRemove
72+
onWorkspaceResourceSelectRef.current = onWorkspaceResourceSelect
73+
})
74+
75+
const stableOnContextAdd = useCallback((context: ChatContext) => {
76+
onContextAddRef.current?.(context)
77+
}, [])
78+
const stableOnContextRemove = useCallback((context: ChatContext) => {
79+
onContextRemoveRef.current?.(context)
80+
}, [])
81+
const stableOnWorkspaceResourceSelect = useCallback((resource: MothershipResource) => {
82+
onWorkspaceResourceSelectRef.current?.(resource)
83+
}, [])
84+
85+
const value = useMemo<ChatSurfaceContextValue>(
86+
() => ({
87+
chatId,
88+
userId,
89+
onContextAdd: stableOnContextAdd,
90+
onContextRemove: stableOnContextRemove,
91+
onWorkspaceResourceSelect: stableOnWorkspaceResourceSelect,
92+
}),
93+
[chatId, userId, stableOnContextAdd, stableOnContextRemove, stableOnWorkspaceResourceSelect]
94+
)
95+
96+
return <ChatSurfaceContext.Provider value={value}>{children}</ChatSurfaceContext.Provider>
97+
}
98+
99+
/**
100+
* Reads the surrounding chat surface. Outside a provider this returns no-op
101+
* callbacks and undefined identity, matching the previous optional-prop
102+
* behavior.
103+
*/
104+
export function useChatSurface(): ChatSurfaceContextValue {
105+
return useContext(ChatSurfaceContext)
106+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { ChatSurfaceProvider, useChatSurface } from './chat-surface-context'

apps/sim/app/workspace/[workspaceId]/home/components/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
export { ChatMessageAttachments } from './chat-message-attachments'
2+
export { ChatSurfaceProvider, useChatSurface } from './chat-surface-context'
23
export { ContextMentionIcon } from './context-mention-icon'
34
export { CreditsChip } from './credits-chip'
45
export {
56
assistantMessageHasRenderableContent,
67
MessageContent,
78
} from './message-content'
89
export { MothershipChat } from './mothership-chat'
10+
export {
11+
MothershipResourcesProvider,
12+
useMothershipResources,
13+
} from './mothership-resources-context'
914
export { MothershipView } from './mothership-view'
1015
export { QueuedMessages } from './queued-messages'
1116
export { SuggestedActions } from './suggested-actions'

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ import { Read as ReadTool, WorkspaceFile } from '@/lib/copilot/generated/tool-ca
66
import { isToolHiddenInUi } from '@/lib/copilot/tools/client/hidden-tools'
77
import { resolveToolDisplay } from '@/lib/copilot/tools/client/store-utils'
88
import { ClientToolCallState } from '@/lib/copilot/tools/client/tool-call-state'
9-
import type { ContentBlock, MothershipResource, OptionItem, ToolCallData } from '../../types'
9+
import { useChatSurface } from '@/app/workspace/[workspaceId]/home/components/chat-surface-context'
10+
import type { ContentBlock, OptionItem, ToolCallData } from '../../types'
1011
import { SUBAGENT_LABELS, TOOL_UI_METADATA } from '../../types'
1112
import type { AgentGroupItem } from './components'
1213
import {
@@ -676,16 +677,15 @@ interface MessageContentProps {
676677
fallbackContent: string
677678
isStreaming: boolean
678679
onOptionSelect?: (id: string) => void
679-
onWorkspaceResourceSelect?: (resource: MothershipResource) => void
680680
}
681681

682682
function MessageContentInner({
683683
blocks,
684684
fallbackContent,
685685
isStreaming = false,
686686
onOptionSelect,
687-
onWorkspaceResourceSelect,
688687
}: MessageContentProps) {
688+
const { onWorkspaceResourceSelect } = useChatSurface()
689689
const parsed = useMemo(() => (blocks.length > 0 ? parseBlocks(blocks) : []), [blocks])
690690

691691
const segments: MessageSegment[] =

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

Lines changed: 68 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef } from '
44
import { cn } from '@/lib/core/utils/cn'
55
import { MessageActions } from '@/app/workspace/[workspaceId]/components'
66
import { ChatMessageAttachments } from '@/app/workspace/[workspaceId]/home/components/chat-message-attachments'
7+
import { ChatSurfaceProvider } from '@/app/workspace/[workspaceId]/home/components/chat-surface-context'
78
import {
89
assistantMessageHasRenderableContent,
910
MessageContent,
@@ -124,20 +125,16 @@ interface AssistantMessageRowProps {
124125
message: ChatMessage
125126
isStreaming: boolean
126127
precedingUserContent?: string
127-
chatId?: string
128128
rowClassName: string
129129
onOptionSelect?: (id: string) => void
130-
onWorkspaceResourceSelect?: (resource: MothershipResource) => void
131130
}
132131

133132
const AssistantMessageRow = memo(function AssistantMessageRow({
134133
message,
135134
isStreaming,
136135
precedingUserContent,
137-
chatId,
138136
rowClassName,
139137
onOptionSelect,
140-
onWorkspaceResourceSelect,
141138
}: AssistantMessageRowProps) {
142139
const blocks = message.contentBlocks ?? EMPTY_BLOCKS
143140
const hasAnyBlocks = blocks.length > 0
@@ -161,13 +158,11 @@ const AssistantMessageRow = memo(function AssistantMessageRow({
161158
fallbackContent={message.content}
162159
isStreaming={isStreaming}
163160
onOptionSelect={onOptionSelect}
164-
onWorkspaceResourceSelect={onWorkspaceResourceSelect}
165161
/>
166162
{showActions && (
167163
<div className='mt-2.5'>
168164
<MessageActions
169165
content={message.content}
170-
chatId={chatId}
171166
userQuery={precedingUserContent}
172167
requestId={message.requestId}
173168
messageId={message.id}
@@ -241,17 +236,12 @@ export function MothershipChat({
241236
const userInputRef = useRef<UserInputHandle>(null)
242237

243238
const onSubmitRef = useRef(onSubmit)
244-
const onWorkspaceResourceSelectRef = useRef(onWorkspaceResourceSelect)
245239
useEffect(() => {
246240
onSubmitRef.current = onSubmit
247-
onWorkspaceResourceSelectRef.current = onWorkspaceResourceSelect
248-
}, [onSubmit, onWorkspaceResourceSelect])
241+
}, [onSubmit])
249242
const stableOnOptionSelect = useCallback((id: string) => {
250243
onSubmitRef.current(id)
251244
}, [])
252-
const stableOnWorkspaceResourceSelect = useCallback((resource: MothershipResource) => {
253-
onWorkspaceResourceSelectRef.current?.(resource)
254-
}, [])
255245

256246
function handleSendQueuedHead() {
257247
const topMessage = messageQueue[0]
@@ -286,75 +276,78 @@ export function MothershipChat({
286276
}, [isStaging, stagedMessageCount, initialScrollBlocked, scrollToBottom])
287277

288278
return (
289-
<div className={cn('flex h-full min-h-0 flex-col', className)}>
290-
<div ref={scrollContainerRef} className={styles.scrollContainer}>
291-
{isLoading && !hasMessages ? (
292-
<MothershipChatSkeleton layout={layout} />
293-
) : (
294-
<div className={styles.content}>
295-
{stagedMessages.map((msg, localIndex) => {
296-
const index = stagedOffset + localIndex
297-
if (msg.role === 'user') {
279+
<ChatSurfaceProvider
280+
chatId={chatId}
281+
userId={userId}
282+
onContextAdd={onContextAdd}
283+
onContextRemove={onContextRemove}
284+
onWorkspaceResourceSelect={onWorkspaceResourceSelect}
285+
>
286+
<div className={cn('flex h-full min-h-0 flex-col', className)}>
287+
<div ref={scrollContainerRef} className={styles.scrollContainer}>
288+
{isLoading && !hasMessages ? (
289+
<MothershipChatSkeleton layout={layout} />
290+
) : (
291+
<div className={styles.content}>
292+
{stagedMessages.map((msg, localIndex) => {
293+
const index = stagedOffset + localIndex
294+
if (msg.role === 'user') {
295+
return (
296+
<UserMessageRow
297+
key={msg.id}
298+
content={msg.content}
299+
contexts={msg.contexts}
300+
attachments={msg.attachments}
301+
rowClassName={styles.userRow}
302+
bubbleClassName={styles.userBubble}
303+
attachmentWidthClassName={styles.attachmentWidth}
304+
/>
305+
)
306+
}
307+
308+
const isLast = index === messages.length - 1
298309
return (
299-
<UserMessageRow
300-
key={msg.id}
301-
content={msg.content}
302-
contexts={msg.contexts}
303-
attachments={msg.attachments}
304-
rowClassName={styles.userRow}
305-
bubbleClassName={styles.userBubble}
306-
attachmentWidthClassName={styles.attachmentWidth}
310+
<AssistantMessageRow
311+
key={assistantTurnKeyByIndex[index] ?? msg.id}
312+
message={msg}
313+
isStreaming={isStreamActive && isLast}
314+
precedingUserContent={precedingUserContentByIndex[index]}
315+
rowClassName={styles.assistantRow}
316+
onOptionSelect={isLast ? stableOnOptionSelect : undefined}
307317
/>
308318
)
309-
}
319+
})}
320+
</div>
321+
)}
322+
</div>
310323

311-
const isLast = index === messages.length - 1
312-
return (
313-
<AssistantMessageRow
314-
key={assistantTurnKeyByIndex[index] ?? msg.id}
315-
message={msg}
316-
isStreaming={isStreamActive && isLast}
317-
precedingUserContent={precedingUserContentByIndex[index]}
318-
chatId={chatId}
319-
rowClassName={styles.assistantRow}
320-
onOptionSelect={isLast ? stableOnOptionSelect : undefined}
321-
onWorkspaceResourceSelect={stableOnWorkspaceResourceSelect}
322-
/>
323-
)
324-
})}
324+
<div
325+
className={cn(styles.footer, animateInput && 'animate-slide-in-bottom')}
326+
onAnimationEnd={animateInput ? onInputAnimationEnd : undefined}
327+
>
328+
<div className={styles.footerInner}>
329+
<QueuedMessages
330+
messageQueue={messageQueue}
331+
editingQueuedId={editingQueuedId}
332+
dispatchingHeadId={dispatchingHeadId}
333+
onRemove={onRemoveQueuedMessage}
334+
onSendNow={onSendQueuedMessage}
335+
onEdit={handleEditQueued}
336+
onCancelEdit={onCancelQueueEdit}
337+
/>
338+
<UserInput
339+
ref={userInputRef}
340+
onSubmit={onSubmit}
341+
isSending={isStreamActive}
342+
onStopGeneration={onStopGeneration}
343+
isInitialView={false}
344+
onSendQueuedHead={handleSendQueuedHead}
345+
onEditQueuedTail={handleEditQueuedTail}
346+
draftScopeKey={draftScopeKey}
347+
/>
325348
</div>
326-
)}
327-
</div>
328-
329-
<div
330-
className={cn(styles.footer, animateInput && 'animate-slide-in-bottom')}
331-
onAnimationEnd={animateInput ? onInputAnimationEnd : undefined}
332-
>
333-
<div className={styles.footerInner}>
334-
<QueuedMessages
335-
messageQueue={messageQueue}
336-
editingQueuedId={editingQueuedId}
337-
dispatchingHeadId={dispatchingHeadId}
338-
onRemove={onRemoveQueuedMessage}
339-
onSendNow={onSendQueuedMessage}
340-
onEdit={handleEditQueued}
341-
onCancelEdit={onCancelQueueEdit}
342-
/>
343-
<UserInput
344-
ref={userInputRef}
345-
onSubmit={onSubmit}
346-
isSending={isStreamActive}
347-
onStopGeneration={onStopGeneration}
348-
isInitialView={false}
349-
userId={userId}
350-
onContextAdd={onContextAdd}
351-
onContextRemove={onContextRemove}
352-
onSendQueuedHead={handleSendQueuedHead}
353-
onEditQueuedTail={handleEditQueuedTail}
354-
draftScopeKey={draftScopeKey}
355-
/>
356349
</div>
357350
</div>
358-
</div>
351+
</ChatSurfaceProvider>
359352
)
360353
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export {
2+
MothershipResourcesProvider,
3+
useMothershipResources,
4+
} from './mothership-resources-context'

0 commit comments

Comments
 (0)