Skip to content

Commit cce740d

Browse files
committed
feat(mothership): add execution logs as a resource type
Adds `log` as a first-class mothership resource type so copilot can open and display workflow execution logs as tabs alongside workflows, tables, files, and knowledge bases. - Add `log` to MothershipResourceType, all Zod enums, and VALID_RESOURCE_TYPES - Register log in RESOURCE_REGISTRY (Library icon) and RESOURCE_INVALIDATORS - Add EmbeddedLog and EmbeddedLogActions components in resource-content - Export WorkflowOutputSection from log-details for reuse in EmbeddedLog - Add log resolution branch in open_resource handler via new getLogById service - Include log id in get_workflow_logs response and extract resources from output - Exclude log from manual add-resource dropdown (enters via copilot tools only) - Regenerate copilot contracts after adding log to open_resource Go enum
1 parent 72e3c69 commit cce740d

File tree

19 files changed

+797
-271
lines changed

19 files changed

+797
-271
lines changed

apps/sim/app/api/copilot/chat/resources/route.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,29 +21,30 @@ const VALID_RESOURCE_TYPES = new Set<ResourceType>([
2121
'workflow',
2222
'knowledgebase',
2323
'folder',
24+
'log',
2425
])
25-
const GENERIC_TITLES = new Set(['Table', 'File', 'Workflow', 'Knowledge Base', 'Folder'])
26+
const GENERIC_TITLES = new Set(['Table', 'File', 'Workflow', 'Knowledge Base', 'Folder', 'Log'])
2627

2728
const AddResourceSchema = z.object({
2829
chatId: z.string(),
2930
resource: z.object({
30-
type: z.enum(['table', 'file', 'workflow', 'knowledgebase', 'folder']),
31+
type: z.enum(['table', 'file', 'workflow', 'knowledgebase', 'folder', 'log']),
3132
id: z.string(),
3233
title: z.string(),
3334
}),
3435
})
3536

3637
const RemoveResourceSchema = z.object({
3738
chatId: z.string(),
38-
resourceType: z.enum(['table', 'file', 'workflow', 'knowledgebase', 'folder']),
39+
resourceType: z.enum(['table', 'file', 'workflow', 'knowledgebase', 'folder', 'log']),
3940
resourceId: z.string(),
4041
})
4142

4243
const ReorderResourcesSchema = z.object({
4344
chatId: z.string(),
4445
resources: z.array(
4546
z.object({
46-
type: z.enum(['table', 'file', 'workflow', 'knowledgebase', 'folder']),
47+
type: z.enum(['table', 'file', 'workflow', 'knowledgebase', 'folder', 'log']),
4748
id: z.string(),
4849
title: z.string(),
4950
})
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { Skeleton } from '@/components/emcn'
2+
3+
const LAYOUT_SKELETON_STYLES = {
4+
'mothership-view': {
5+
content: 'mx-auto max-w-[42rem] space-y-6',
6+
userRow: 'flex flex-col items-end gap-[6px] pt-3',
7+
},
8+
'copilot-view': {
9+
content: 'space-y-4',
10+
userRow: 'flex flex-col items-end gap-[6px] pt-2',
11+
},
12+
} as const
13+
14+
interface MothershipChatSkeletonProps {
15+
layout?: 'mothership-view' | 'copilot-view'
16+
}
17+
18+
/**
19+
* Skeleton content rendered inside MothershipChat's scroll area
20+
* while chat history is being fetched.
21+
*/
22+
export function MothershipChatSkeleton({
23+
layout = 'mothership-view',
24+
}: MothershipChatSkeletonProps) {
25+
const styles = LAYOUT_SKELETON_STYLES[layout]
26+
27+
return (
28+
<div className={styles.content}>
29+
<div className={styles.userRow}>
30+
<Skeleton className='h-[40px] w-[55%] rounded-[16px]' />
31+
</div>
32+
33+
<div className='space-y-3'>
34+
<Skeleton className='h-[14px] w-[90%] rounded-[4px]' />
35+
<Skeleton className='h-[14px] w-[75%] rounded-[4px]' />
36+
<Skeleton className='h-[14px] w-[82%] rounded-[4px]' />
37+
<Skeleton className='h-[14px] w-[40%] rounded-[4px]' />
38+
</div>
39+
40+
<div className={styles.userRow}>
41+
<Skeleton className='h-[32px] w-[40%] rounded-[16px]' />
42+
</div>
43+
44+
<div className='space-y-3'>
45+
<Skeleton className='h-[14px] w-[85%] rounded-[4px]' />
46+
<Skeleton className='h-[14px] w-[70%] rounded-[4px]' />
47+
<Skeleton className='h-[14px] w-[60%] rounded-[4px]' />
48+
</div>
49+
</div>
50+
)
51+
}

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

Lines changed: 66 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,13 @@ import type {
2020
} from '@/app/workspace/[workspaceId]/home/types'
2121
import { useAutoScroll } from '@/hooks/use-auto-scroll'
2222
import type { ChatContext } from '@/stores/panel'
23+
import { MothershipChatSkeleton } from './mothership-chat-skeleton'
2324

2425
interface MothershipChatProps {
2526
messages: ChatMessage[]
2627
isSending: boolean
2728
isReconnecting?: boolean
29+
isLoading?: boolean
2830
onSubmit: (
2931
text: string,
3032
fileAttachments?: FileAttachmentForApi[],
@@ -77,6 +79,7 @@ export function MothershipChat({
7779
messages,
7880
isSending,
7981
isReconnecting = false,
82+
isLoading = false,
8083
onSubmit,
8184
onStopGeneration,
8285
messageQueue,
@@ -129,71 +132,75 @@ export function MothershipChat({
129132
return (
130133
<div className={cn('flex h-full min-h-0 flex-col', className)}>
131134
<div ref={scrollContainerRef} className={styles.scrollContainer}>
132-
<div className={styles.content}>
133-
{messages.map((msg, index) => {
134-
if (msg.role === 'user') {
135-
const hasAttachments = Boolean(msg.attachments?.length)
136-
return (
137-
<div key={msg.id} className={styles.userRow}>
138-
{hasAttachments && (
139-
<ChatMessageAttachments
140-
attachments={msg.attachments ?? []}
141-
align='end'
142-
className={styles.attachmentWidth}
143-
/>
144-
)}
145-
<div className={styles.userBubble}>
146-
<UserMessageContent content={msg.content} contexts={msg.contexts} />
135+
{isLoading && !hasMessages ? (
136+
<MothershipChatSkeleton layout={layout} />
137+
) : (
138+
<div className={styles.content}>
139+
{messages.map((msg, index) => {
140+
if (msg.role === 'user') {
141+
const hasAttachments = Boolean(msg.attachments?.length)
142+
return (
143+
<div key={msg.id} className={styles.userRow}>
144+
{hasAttachments && (
145+
<ChatMessageAttachments
146+
attachments={msg.attachments ?? []}
147+
align='end'
148+
className={styles.attachmentWidth}
149+
/>
150+
)}
151+
<div className={styles.userBubble}>
152+
<UserMessageContent content={msg.content} contexts={msg.contexts} />
153+
</div>
147154
</div>
148-
</div>
149-
)
150-
}
155+
)
156+
}
151157

152-
const hasAnyBlocks = Boolean(msg.contentBlocks?.length)
153-
const hasRenderableAssistant = assistantMessageHasRenderableContent(
154-
msg.contentBlocks ?? [],
155-
msg.content ?? ''
156-
)
157-
const isLastAssistant = index === messages.length - 1
158-
const isThisStreaming = isStreamActive && isLastAssistant
158+
const hasAnyBlocks = Boolean(msg.contentBlocks?.length)
159+
const hasRenderableAssistant = assistantMessageHasRenderableContent(
160+
msg.contentBlocks ?? [],
161+
msg.content ?? ''
162+
)
163+
const isLastAssistant = index === messages.length - 1
164+
const isThisStreaming = isStreamActive && isLastAssistant
159165

160-
if (!hasAnyBlocks && !msg.content?.trim() && isThisStreaming) {
161-
return <PendingTagIndicator key={msg.id} />
162-
}
166+
if (!hasAnyBlocks && !msg.content?.trim() && isThisStreaming) {
167+
return <PendingTagIndicator key={msg.id} />
168+
}
163169

164-
if (!hasRenderableAssistant && !msg.content?.trim() && !isThisStreaming) {
165-
return null
166-
}
170+
if (!hasRenderableAssistant && !msg.content?.trim() && !isThisStreaming) {
171+
return null
172+
}
167173

168-
const isLastMessage = index === messages.length - 1
169-
const precedingUserMsg = [...messages]
170-
.slice(0, index)
171-
.reverse()
172-
.find((m) => m.role === 'user')
174+
const isLastMessage = index === messages.length - 1
175+
const precedingUserMsg = [...messages]
176+
.slice(0, index)
177+
.reverse()
178+
.find((m) => m.role === 'user')
173179

174-
return (
175-
<div key={msg.id} className={styles.assistantRow}>
176-
<MessageContent
177-
blocks={msg.contentBlocks || []}
178-
fallbackContent={msg.content}
179-
isStreaming={isThisStreaming}
180-
onOptionSelect={isLastMessage && !isStreamActive ? onSubmit : undefined}
181-
onWorkspaceResourceSelect={onWorkspaceResourceSelect}
182-
/>
183-
{!isThisStreaming && (msg.content || msg.contentBlocks?.length) && (
184-
<div className='mt-2.5'>
185-
<MessageActions
186-
content={msg.content}
187-
chatId={chatId}
188-
userQuery={precedingUserMsg?.content}
189-
requestId={msg.requestId}
190-
/>
191-
</div>
192-
)}
193-
</div>
194-
)
195-
})}
196-
</div>
180+
return (
181+
<div key={msg.id} className={styles.assistantRow}>
182+
<MessageContent
183+
blocks={msg.contentBlocks || []}
184+
fallbackContent={msg.content}
185+
isStreaming={isThisStreaming}
186+
onOptionSelect={isLastMessage && !isStreamActive ? onSubmit : undefined}
187+
onWorkspaceResourceSelect={onWorkspaceResourceSelect}
188+
/>
189+
{!isThisStreaming && (msg.content || msg.contentBlocks?.length) && (
190+
<div className='mt-2.5'>
191+
<MessageActions
192+
content={msg.content}
193+
chatId={chatId}
194+
userQuery={precedingUserMsg?.content}
195+
requestId={msg.requestId}
196+
/>
197+
</div>
198+
)}
199+
</div>
200+
)
201+
})}
202+
</div>
203+
)}
197204
</div>
198205

199206
<div

0 commit comments

Comments
 (0)