improvement(mothership): message queueing for home chat (#3576)

* improvement(mothership): message queueing for home chat

* fix(mothership): address PR review — move FileAttachmentForApi to types, defer onEditValueConsumed to effect, await sendMessage in sendNow

* fix(mothership): replace updater side-effect with useEffect ref sync, move sendMessageRef to useLayoutEffect

* fix(mothership): clear message queue on chat switch while sending

* fix(mothership): remove stale isSending from handleKeyDown deps

* fix(mothership): guard sendNow against double-click duplicate sends

* fix(mothership): simplify queue callbacks — drop redundant deps and guard ref

- Remove `setMessageQueue` from useCallback deps (stable setter, never changes)
- Replace `sendNowProcessingRef` double-click guard with eager `messageQueueRef` update
- Simplify `editQueuedMessage` with same eager-ref pattern for consistency

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(mothership): clear edit value on nav, stop queue drain on send failure

- Reset editingInputValue when chatId changes so stale edit text
  doesn't leak into the next chat
- Pass error flag to finalize so queue is cleared (not drained) when
  sendMessage fails — prevents cascading failures on auth expiry or
  rate limiting from silently consuming every queued message

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(mothership): eagerly update messageQueueRef in removeFromQueue

Match the pattern used by sendNow and editQueuedMessage — update the
ref synchronously so finalize's microtask cannot read a stale queue
and drain a message the user just removed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(mothership): mark onSendNow as explicit fire-and-forget

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Waleed
2026-03-14 06:24:00 -07:00
committed by GitHub
parent d06aa1de7e
commit b2d146ca0a
7 changed files with 437 additions and 62 deletions

View File

@@ -1,5 +1,6 @@
export { MessageContent } from './message-content'
export { MothershipView } from './mothership-view'
export { QueuedMessages } from './queued-messages'
export { TemplatePrompts } from './template-prompts'
export { UserInput } from './user-input'
export { UserMessageContent } from './user-message-content'

View File

@@ -0,0 +1 @@
export { QueuedMessages } from './queued-messages'

View File

@@ -0,0 +1,113 @@
'use client'
import { useState } from 'react'
import { ArrowUp, ChevronDown, ChevronRight, Pencil, Trash2 } from 'lucide-react'
import { Tooltip } from '@/components/emcn'
import type { QueuedMessage } from '@/app/workspace/[workspaceId]/home/types'
interface QueuedMessagesProps {
messageQueue: QueuedMessage[]
onRemove: (id: string) => void
onSendNow: (id: string) => Promise<void>
onEdit: (id: string) => void
}
export function QueuedMessages({ messageQueue, onRemove, onSendNow, onEdit }: QueuedMessagesProps) {
const [isExpanded, setIsExpanded] = useState(true)
if (messageQueue.length === 0) return null
return (
<div className='-mb-[12px] mx-[14px] overflow-hidden rounded-t-[16px] border border-[var(--border-1)] border-b-0 bg-[var(--surface-2)] pb-[12px] dark:bg-[var(--surface-3)]'>
<button
type='button'
onClick={() => setIsExpanded(!isExpanded)}
className='flex w-full items-center gap-[6px] px-[14px] py-[8px] transition-colors hover:bg-black/[0.03] dark:hover:bg-white/[0.03]'
>
{isExpanded ? (
<ChevronDown className='h-[14px] w-[14px] text-[var(--text-tertiary)]' />
) : (
<ChevronRight className='h-[14px] w-[14px] text-[var(--text-tertiary)]' />
)}
<span className='font-medium text-[13px] text-[var(--text-secondary)]'>
{messageQueue.length} Queued
</span>
</button>
{isExpanded && (
<div>
{messageQueue.map((msg) => (
<div
key={msg.id}
className='flex items-center gap-[8px] px-[14px] py-[6px] transition-colors hover:bg-black/[0.03] dark:hover:bg-white/[0.03]'
>
<div className='flex h-[16px] w-[16px] shrink-0 items-center justify-center'>
<div className='h-[10px] w-[10px] rounded-full border-[1.5px] border-[var(--text-tertiary)]/40' />
</div>
<div className='min-w-0 flex-1'>
<p className='truncate text-[13px] text-[var(--text-primary)]'>{msg.content}</p>
</div>
<div className='flex shrink-0 items-center gap-[2px]'>
<Tooltip.Root>
<Tooltip.Trigger asChild>
<button
type='button'
onClick={(e) => {
e.stopPropagation()
onEdit(msg.id)
}}
className='rounded-[6px] p-[5px] text-[var(--text-tertiary)] transition-colors hover:bg-black/[0.06] hover:text-[var(--text-primary)] dark:hover:bg-white/[0.06]'
>
<Pencil className='h-[13px] w-[13px]' />
</button>
</Tooltip.Trigger>
<Tooltip.Content side='top' sideOffset={4}>
Edit queued message
</Tooltip.Content>
</Tooltip.Root>
<Tooltip.Root>
<Tooltip.Trigger asChild>
<button
type='button'
onClick={(e) => {
e.stopPropagation()
void onSendNow(msg.id)
}}
className='rounded-[6px] p-[5px] text-[var(--text-tertiary)] transition-colors hover:bg-black/[0.06] hover:text-[var(--text-primary)] dark:hover:bg-white/[0.06]'
>
<ArrowUp className='h-[13px] w-[13px]' />
</button>
</Tooltip.Trigger>
<Tooltip.Content side='top' sideOffset={4}>
Send now
</Tooltip.Content>
</Tooltip.Root>
<Tooltip.Root>
<Tooltip.Trigger asChild>
<button
type='button'
onClick={(e) => {
e.stopPropagation()
onRemove(msg.id)
}}
className='rounded-[6px] p-[5px] text-[var(--text-tertiary)] transition-colors hover:bg-black/[0.06] hover:text-[var(--text-primary)] dark:hover:bg-white/[0.06]'
>
<Trash2 className='h-[13px] w-[13px]' />
</button>
</Tooltip.Trigger>
<Tooltip.Content side='top' sideOffset={4}>
Remove from queue
</Tooltip.Content>
</Tooltip.Root>
</div>
</div>
))}
</div>
)}
</div>
)
}

View File

@@ -64,7 +64,10 @@ import { cn } from '@/lib/core/utils/cn'
import { CHAT_ACCEPT_ATTRIBUTE } from '@/lib/uploads/utils/validation'
import { useAvailableResources } from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/add-resource-dropdown'
import { getResourceConfig } from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-registry'
import type { MothershipResource } from '@/app/workspace/[workspaceId]/home/types'
import type {
FileAttachmentForApi,
MothershipResource,
} from '@/app/workspace/[workspaceId]/home/types'
import {
useContextManagement,
useFileAttachments,
@@ -125,9 +128,17 @@ function autoResizeTextarea(e: React.FormEvent<HTMLTextAreaElement>, maxHeight:
function mapResourceToContext(resource: MothershipResource): ChatContext {
switch (resource.type) {
case 'workflow':
return { kind: 'workflow', workflowId: resource.id, label: resource.title }
return {
kind: 'workflow',
workflowId: resource.id,
label: resource.title,
}
case 'knowledgebase':
return { kind: 'knowledge', knowledgeId: resource.id, label: resource.title }
return {
kind: 'knowledge',
knowledgeId: resource.id,
label: resource.title,
}
case 'table':
return { kind: 'table', tableId: resource.id, label: resource.title }
case 'file':
@@ -137,16 +148,12 @@ function mapResourceToContext(resource: MothershipResource): ChatContext {
}
}
export interface FileAttachmentForApi {
id: string
key: string
filename: string
media_type: string
size: number
}
export type { FileAttachmentForApi } from '@/app/workspace/[workspaceId]/home/types'
interface UserInputProps {
defaultValue?: string
editValue?: string
onEditValueConsumed?: () => void
onSubmit: (
text: string,
fileAttachments?: FileAttachmentForApi[],
@@ -161,6 +168,8 @@ interface UserInputProps {
export function UserInput({
defaultValue = '',
editValue,
onEditValueConsumed,
onSubmit,
isSending,
onStopGeneration,
@@ -176,9 +185,27 @@ export function UserInput({
const [plusMenuActiveIndex, setPlusMenuActiveIndex] = useState(0)
const overlayRef = useRef<HTMLDivElement>(null)
const [prevDefaultValue, setPrevDefaultValue] = useState(defaultValue)
if (defaultValue && defaultValue !== prevDefaultValue) {
setPrevDefaultValue(defaultValue)
setValue(defaultValue)
} else if (!defaultValue && prevDefaultValue) {
setPrevDefaultValue(defaultValue)
}
const [prevEditValue, setPrevEditValue] = useState(editValue)
if (editValue && editValue !== prevEditValue) {
setPrevEditValue(editValue)
setValue(editValue)
} else if (!editValue && prevEditValue) {
setPrevEditValue(editValue)
}
useEffect(() => {
if (defaultValue) setValue(defaultValue)
}, [defaultValue])
if (editValue) {
onEditValueConsumed?.()
}
}, [editValue, onEditValueConsumed])
const animatedPlaceholder = useAnimatedPlaceholder(isInitialView)
const placeholder = isInitialView ? animatedPlaceholder : 'Send message to Sim'
@@ -393,9 +420,7 @@ export function UserInput({
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing) {
e.preventDefault()
if (!isSending) {
handleSubmit()
}
handleSubmit()
return
}
@@ -461,7 +486,7 @@ export function UserInput({
}
}
},
[handleSubmit, isSending, mentionTokensWithContext, value, textareaRef]
[handleSubmit, mentionTokensWithContext, value, textareaRef]
)
const handleInputChange = useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
@@ -637,7 +662,9 @@ export function UserInput({
<span
key={`mention-${i}-${range.start}-${range.end}`}
className='rounded-[5px] bg-[var(--surface-5)] py-[2px]'
style={{ boxShadow: '-2px 0 0 var(--surface-5), 2px 0 0 var(--surface-5)' }}
style={{
boxShadow: '-2px 0 0 var(--surface-5), 2px 0 0 var(--surface-5)',
}}
>
<span className='relative'>
<span className='invisible'>{range.token.charAt(0)}</span>
@@ -662,7 +689,7 @@ export function UserInput({
<div
onClick={handleContainerClick}
className={cn(
'relative mx-auto w-full max-w-[42rem] cursor-text rounded-[20px] border border-[var(--border-1)] bg-[var(--white)] px-[10px] py-[8px] dark:bg-[var(--surface-4)]',
'relative z-10 mx-auto w-full max-w-[42rem] cursor-text rounded-[20px] border border-[var(--border-1)] bg-[var(--white)] px-[10px] py-[8px] dark:bg-[var(--surface-4)]',
isInitialView && 'shadow-sm'
)}
onDragEnter={files.handleDragEnter}
@@ -818,7 +845,11 @@ export function UserInput({
)}
onMouseEnter={() => setPlusMenuActiveIndex(index)}
onClick={() => {
handleResourceSelect({ type, id: item.id, title: item.name })
handleResourceSelect({
type,
id: item.id,
title: item.name,
})
setPlusMenuOpen(false)
setPlusMenuSearch('')
setPlusMenuActiveIndex(0)

View File

@@ -19,14 +19,14 @@ import type { ChatContext } from '@/stores/panel'
import {
MessageContent,
MothershipView,
QueuedMessages,
TemplatePrompts,
UserInput,
UserMessageContent,
} from './components'
import { PendingTagIndicator } from './components/message-content/components/special-tags'
import type { FileAttachmentForApi } from './components/user-input/user-input'
import { useAutoScroll, useChat } from './hooks'
import type { MothershipResource, MothershipResourceType } from './types'
import type { FileAttachmentForApi, MothershipResource, MothershipResourceType } from './types'
const logger = createLogger('Home')
@@ -183,8 +183,29 @@ export function Home({ chatId }: HomeProps = {}) {
addResource,
removeResource,
reorderResources,
messageQueue,
removeFromQueue,
sendNow,
editQueuedMessage,
} = useChat(workspaceId, chatId, { onResourceEvent: handleResourceEvent })
const [editingInputValue, setEditingInputValue] = useState('')
const clearEditingValue = useCallback(() => setEditingInputValue(''), [])
const handleEditQueuedMessage = useCallback(
(id: string) => {
const msg = editQueuedMessage(id)
if (msg) {
setEditingInputValue(msg.content)
}
},
[editQueuedMessage]
)
useEffect(() => {
setEditingInputValue('')
}, [chatId])
useEffect(() => {
wasSendingRef.current = false
if (resolvedChatId) markRead(resolvedChatId)
@@ -419,6 +440,12 @@ export function Home({ chatId }: HomeProps = {}) {
<div className='flex-shrink-0 px-[24px] pb-[16px]'>
<div className='mx-auto max-w-[42rem]'>
<QueuedMessages
messageQueue={messageQueue}
onRemove={removeFromQueue}
onSendNow={sendNow}
onEdit={handleEditQueuedMessage}
/>
<UserInput
onSubmit={handleSubmit}
isSending={isSending}
@@ -426,6 +453,8 @@ export function Home({ chatId }: HomeProps = {}) {
isInitialView={false}
userId={session?.user?.id}
onContextAdd={handleContextAdd}
editValue={editingInputValue}
onEditValueConsumed={clearEditingValue}
/>
</div>
</div>

View File

@@ -1,4 +1,4 @@
import { useCallback, useEffect, useRef, useState } from 'react'
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'
import { createLogger } from '@sim/logger'
import { useQueryClient } from '@tanstack/react-query'
import { usePathname } from 'next/navigation'
@@ -28,14 +28,15 @@ import { useFolderStore } from '@/stores/folders/store'
import type { ChatContext } from '@/stores/panel'
import { useTerminalConsoleStore } from '@/stores/terminal'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import type { FileAttachmentForApi } from '../components/user-input/user-input'
import type {
ChatMessage,
ChatMessageAttachment,
ContentBlock,
ContentBlockType,
FileAttachmentForApi,
MothershipResource,
MothershipResourceType,
QueuedMessage,
SSEPayload,
SSEPayloadData,
ToolCallStatus,
@@ -58,6 +59,10 @@ export interface UseChatReturn {
addResource: (resource: MothershipResource) => boolean
removeResource: (resourceType: MothershipResourceType, resourceId: string) => void
reorderResources: (resources: MothershipResource[]) => void
messageQueue: QueuedMessage[]
removeFromQueue: (id: string) => void
sendNow: (id: string) => Promise<void>
editQueuedMessage: (id: string) => QueuedMessage | undefined
}
const STATE_TO_STATUS: Record<string, ToolCallStatus> = {
@@ -101,7 +106,11 @@ function mapStoredToolCall(tc: TaskStoredToolCall): ContentBlock {
displayTitle: resolvedStatus === 'cancelled' ? 'Stopped by user' : undefined,
result:
tc.result != null
? { success: tc.status === 'success', output: tc.result, error: tc.error }
? {
success: tc.status === 'success',
output: tc.result,
error: tc.error,
}
: undefined,
},
}
@@ -252,6 +261,14 @@ export function useChat(
const activeResourceIdRef = useRef(activeResourceId)
activeResourceIdRef.current = activeResourceId
const [messageQueue, setMessageQueue] = useState<QueuedMessage[]>([])
const messageQueueRef = useRef<QueuedMessage[]>([])
useEffect(() => {
messageQueueRef.current = messageQueue
}, [messageQueue])
const sendMessageRef = useRef<UseChatReturn['sendMessage']>(async () => {})
const abortControllerRef = useRef<AbortController | null>(null)
const chatIdRef = useRef<string | undefined>(initialChatId)
const appliedChatIdRef = useRef<string | undefined>(undefined)
@@ -303,6 +320,7 @@ export function useChat(
if (sendingRef.current) {
chatIdRef.current = initialChatId
setResolvedChatId(initialChatId)
setMessageQueue([])
return
}
chatIdRef.current = initialChatId
@@ -313,6 +331,7 @@ export function useChat(
setIsSending(false)
setResources([])
setActiveResourceId(null)
setMessageQueue([])
}, [initialChatId])
useEffect(() => {
@@ -329,6 +348,7 @@ export function useChat(
setIsSending(false)
setResources([])
setActiveResourceId(null)
setMessageQueue([])
}, [isHomePage])
useEffect(() => {
@@ -419,7 +439,9 @@ export function useChat(
const isNewChat = !chatIdRef.current
chatIdRef.current = parsed.chatId
setResolvedChatId(parsed.chatId)
queryClient.invalidateQueries({ queryKey: taskKeys.list(workspaceId) })
queryClient.invalidateQueries({
queryKey: taskKeys.list(workspaceId),
})
if (isNewChat) {
const userMsg = pendingUserMsgRef.current
const activeStreamId = streamIdRef.current
@@ -427,7 +449,13 @@ export function useChat(
queryClient.setQueryData<TaskChatHistory>(taskKeys.detail(parsed.chatId), {
id: parsed.chatId,
title: null,
messages: [{ id: userMsg.id, role: 'user', content: userMsg.content }],
messages: [
{
id: userMsg.id,
role: 'user',
content: userMsg.content,
},
],
activeStreamId,
resources: [],
})
@@ -619,7 +647,9 @@ export function useChat(
break
}
case 'title_updated': {
queryClient.invalidateQueries({ queryKey: taskKeys.list(workspaceId) })
queryClient.invalidateQueries({
queryKey: taskKeys.list(workspaceId),
})
break
}
case 'error': {
@@ -689,17 +719,37 @@ export function useChat(
const invalidateChatQueries = useCallback(() => {
const activeChatId = chatIdRef.current
if (activeChatId) {
queryClient.invalidateQueries({ queryKey: taskKeys.detail(activeChatId) })
queryClient.invalidateQueries({
queryKey: taskKeys.detail(activeChatId),
})
}
queryClient.invalidateQueries({ queryKey: taskKeys.list(workspaceId) })
}, [workspaceId, queryClient])
const finalize = useCallback(() => {
sendingRef.current = false
setIsSending(false)
abortControllerRef.current = null
invalidateChatQueries()
}, [invalidateChatQueries])
const finalize = useCallback(
(options?: { error?: boolean }) => {
sendingRef.current = false
setIsSending(false)
abortControllerRef.current = null
invalidateChatQueries()
if (options?.error) {
setMessageQueue([])
return
}
const next = messageQueueRef.current[0]
if (next) {
setMessageQueue((prev) => prev.filter((m) => m.id !== next.id))
const gen = streamGenRef.current
queueMicrotask(() => {
if (streamGenRef.current !== gen) return
sendMessageRef.current(next.content, next.fileAttachments, next.contexts)
})
}
},
[invalidateChatQueries]
)
useEffect(() => {
const activeStreamId = chatHistory?.activeStreamId
@@ -714,7 +764,12 @@ export function useChat(
const assistantId = crypto.randomUUID()
setMessages((prev) => [
...prev,
{ id: assistantId, role: 'assistant' as const, content: '', contentBlocks: [] },
{
id: assistantId,
role: 'assistant' as const,
content: '',
contentBlocks: [],
},
])
const reconnect = async () => {
@@ -745,9 +800,15 @@ export function useChat(
if (!message.trim() || !workspaceId) return
if (sendingRef.current) {
await persistPartialResponse()
const queued: QueuedMessage = {
id: crypto.randomUUID(),
content: message,
fileAttachments,
contexts,
}
setMessageQueue((prev) => [...prev, queued])
return
}
abortControllerRef.current?.abort()
const gen = ++streamGenRef.current
@@ -854,14 +915,20 @@ export function useChat(
} catch (err) {
if (err instanceof Error && err.name === 'AbortError') return
setError(err instanceof Error ? err.message : 'Failed to send message')
} finally {
if (streamGenRef.current === gen) {
finalize()
finalize({ error: true })
}
return
}
if (streamGenRef.current === gen) {
finalize()
}
},
[workspaceId, queryClient, processSSEStream, finalize, persistPartialResponse]
[workspaceId, queryClient, processSSEStream, finalize]
)
useLayoutEffect(() => {
sendMessageRef.current = sendMessage
})
const stopGeneration = useCallback(async () => {
if (sendingRef.current) {
@@ -943,6 +1010,32 @@ export function useChat(
}
}, [invalidateChatQueries, persistPartialResponse, executionStream])
const removeFromQueue = useCallback((id: string) => {
messageQueueRef.current = messageQueueRef.current.filter((m) => m.id !== id)
setMessageQueue((prev) => prev.filter((m) => m.id !== id))
}, [])
const sendNow = useCallback(
async (id: string) => {
const msg = messageQueueRef.current.find((m) => m.id === id)
if (!msg) return
// Eagerly update ref so a rapid second click finds the message already gone
messageQueueRef.current = messageQueueRef.current.filter((m) => m.id !== id)
await stopGeneration()
setMessageQueue((prev) => prev.filter((m) => m.id !== id))
await sendMessage(msg.content, msg.fileAttachments, msg.contexts)
},
[stopGeneration, sendMessage]
)
const editQueuedMessage = useCallback((id: string): QueuedMessage | undefined => {
const msg = messageQueueRef.current.find((m) => m.id === id)
if (!msg) return undefined
messageQueueRef.current = messageQueueRef.current.filter((m) => m.id !== id)
setMessageQueue((prev) => prev.filter((m) => m.id !== id))
return msg
}, [])
useEffect(() => {
return () => {
streamGenRef.current++
@@ -968,5 +1061,9 @@ export function useChat(
addResource,
removeResource,
reorderResources,
messageQueue,
removeFromQueue,
sendNow,
editQueuedMessage,
}
}

View File

@@ -1,6 +1,25 @@
import type { MothershipResourceType } from '@/lib/copilot/resource-types'
import type { ChatContext } from '@/stores/panel'
export type { MothershipResource, MothershipResourceType } from '@/lib/copilot/resource-types'
export type {
MothershipResource,
MothershipResourceType,
} from '@/lib/copilot/resource-types'
export interface FileAttachmentForApi {
id: string
key: string
filename: string
media_type: string
size: number
}
export interface QueuedMessage {
id: string
content: string
fileAttachments?: FileAttachmentForApi[]
contexts?: ChatContext[]
}
/**
* SSE event types emitted by the Go orchestrator backend.
@@ -203,38 +222,122 @@ export interface ToolUIMetadata {
* fallback metadata for tools that arrive via `tool_generating` without `ui`.
*/
export const TOOL_UI_METADATA: Partial<Record<MothershipToolName, ToolUIMetadata>> = {
glob: { title: 'Searching files', phaseLabel: 'Workspace', phase: 'workspace' },
grep: { title: 'Searching code', phaseLabel: 'Workspace', phase: 'workspace' },
glob: {
title: 'Searching files',
phaseLabel: 'Workspace',
phase: 'workspace',
},
grep: {
title: 'Searching code',
phaseLabel: 'Workspace',
phase: 'workspace',
},
read: { title: 'Reading file', phaseLabel: 'Workspace', phase: 'workspace' },
search_online: { title: 'Searching online', phaseLabel: 'Search', phase: 'search' },
scrape_page: { title: 'Scraping page', phaseLabel: 'Search', phase: 'search' },
get_page_contents: { title: 'Getting page contents', phaseLabel: 'Search', phase: 'search' },
search_library_docs: { title: 'Searching library docs', phaseLabel: 'Search', phase: 'search' },
manage_mcp_tool: { title: 'Managing MCP tool', phaseLabel: 'Management', phase: 'management' },
manage_skill: { title: 'Managing skill', phaseLabel: 'Management', phase: 'management' },
user_memory: { title: 'Accessing memory', phaseLabel: 'Management', phase: 'management' },
function_execute: { title: 'Running code', phaseLabel: 'Code', phase: 'execution' },
superagent: { title: 'Executing action', phaseLabel: 'Action', phase: 'execution' },
user_table: { title: 'Managing table', phaseLabel: 'Resource', phase: 'resource' },
workspace_file: { title: 'Managing file', phaseLabel: 'Resource', phase: 'resource' },
create_workflow: { title: 'Creating workflow', phaseLabel: 'Resource', phase: 'resource' },
edit_workflow: { title: 'Editing workflow', phaseLabel: 'Resource', phase: 'resource' },
search_online: {
title: 'Searching online',
phaseLabel: 'Search',
phase: 'search',
},
scrape_page: {
title: 'Scraping page',
phaseLabel: 'Search',
phase: 'search',
},
get_page_contents: {
title: 'Getting page contents',
phaseLabel: 'Search',
phase: 'search',
},
search_library_docs: {
title: 'Searching library docs',
phaseLabel: 'Search',
phase: 'search',
},
manage_mcp_tool: {
title: 'Managing MCP tool',
phaseLabel: 'Management',
phase: 'management',
},
manage_skill: {
title: 'Managing skill',
phaseLabel: 'Management',
phase: 'management',
},
user_memory: {
title: 'Accessing memory',
phaseLabel: 'Management',
phase: 'management',
},
function_execute: {
title: 'Running code',
phaseLabel: 'Code',
phase: 'execution',
},
superagent: {
title: 'Executing action',
phaseLabel: 'Action',
phase: 'execution',
},
user_table: {
title: 'Managing table',
phaseLabel: 'Resource',
phase: 'resource',
},
workspace_file: {
title: 'Managing file',
phaseLabel: 'Resource',
phase: 'resource',
},
create_workflow: {
title: 'Creating workflow',
phaseLabel: 'Resource',
phase: 'resource',
},
edit_workflow: {
title: 'Editing workflow',
phaseLabel: 'Resource',
phase: 'resource',
},
build: { title: 'Building', phaseLabel: 'Build', phase: 'subagent' },
run: { title: 'Running', phaseLabel: 'Run', phase: 'subagent' },
deploy: { title: 'Deploying', phaseLabel: 'Deploy', phase: 'subagent' },
auth: { title: 'Connecting credentials', phaseLabel: 'Auth', phase: 'subagent' },
knowledge: { title: 'Managing knowledge', phaseLabel: 'Knowledge', phase: 'subagent' },
knowledge_base: { title: 'Managing knowledge base', phaseLabel: 'Resource', phase: 'resource' },
auth: {
title: 'Connecting credentials',
phaseLabel: 'Auth',
phase: 'subagent',
},
knowledge: {
title: 'Managing knowledge',
phaseLabel: 'Knowledge',
phase: 'subagent',
},
knowledge_base: {
title: 'Managing knowledge base',
phaseLabel: 'Resource',
phase: 'resource',
},
table: { title: 'Managing tables', phaseLabel: 'Table', phase: 'subagent' },
job: { title: 'Managing jobs', phaseLabel: 'Job', phase: 'subagent' },
agent: { title: 'Agent action', phaseLabel: 'Agent', phase: 'subagent' },
custom_tool: { title: 'Creating tool', phaseLabel: 'Tool', phase: 'subagent' },
custom_tool: {
title: 'Creating tool',
phaseLabel: 'Tool',
phase: 'subagent',
},
research: { title: 'Researching', phaseLabel: 'Research', phase: 'subagent' },
plan: { title: 'Planning', phaseLabel: 'Plan', phase: 'subagent' },
debug: { title: 'Debugging', phaseLabel: 'Debug', phase: 'subagent' },
edit: { title: 'Editing workflow', phaseLabel: 'Edit', phase: 'subagent' },
fast_edit: { title: 'Editing workflow', phaseLabel: 'Edit', phase: 'subagent' },
open_resource: { title: 'Opening resource', phaseLabel: 'Resource', phase: 'resource' },
fast_edit: {
title: 'Editing workflow',
phaseLabel: 'Edit',
phase: 'subagent',
},
open_resource: {
title: 'Opening resource',
phaseLabel: 'Resource',
phase: 'resource',
},
}
export interface SSEPayloadUI {