mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
chat metadata
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
import type { MothershipToolName, ToolCallStatus, ToolPhase } from '../../../../types'
|
||||
import { TOOL_UI_METADATA } from '../../../../types'
|
||||
import type { MothershipToolName, SubagentName, ToolCallStatus, ToolPhase } from '../../../../types'
|
||||
import { SUBAGENT_LABELS, TOOL_UI_METADATA } from '../../../../types'
|
||||
import { getToolIcon } from '../../utils'
|
||||
|
||||
const STATUS_STYLES: Record<ToolCallStatus, string> = {
|
||||
@@ -24,14 +24,18 @@ interface ToolCallProps {
|
||||
displayTitle?: string
|
||||
status: ToolCallStatus
|
||||
phaseLabel?: string
|
||||
calledBy?: string
|
||||
}
|
||||
|
||||
export function ToolCall({ toolName, displayTitle, status, phaseLabel }: ToolCallProps) {
|
||||
export function ToolCall({ toolName, displayTitle, status, phaseLabel, calledBy }: ToolCallProps) {
|
||||
const metadata = TOOL_UI_METADATA[toolName as MothershipToolName]
|
||||
const resolvedTitle = displayTitle || metadata?.title || toolName
|
||||
const resolvedPhase = phaseLabel || metadata?.phaseLabel
|
||||
const resolvedPhaseType = metadata?.phase
|
||||
const Icon = getToolIcon(toolName)
|
||||
const callerLabel = calledBy
|
||||
? (SUBAGENT_LABELS[calledBy as SubagentName] ?? calledBy)
|
||||
: 'Mothership'
|
||||
|
||||
return (
|
||||
<div className='flex items-center gap-[6px]'>
|
||||
@@ -48,6 +52,7 @@ export function ToolCall({ toolName, displayTitle, status, phaseLabel }: ToolCal
|
||||
{resolvedPhase}
|
||||
</span>
|
||||
)}
|
||||
<span className='text-[11px] text-[var(--text-quaternary)]'>via {callerLabel}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ interface ToolCallSegment {
|
||||
displayTitle?: string
|
||||
status: ToolCallStatus
|
||||
phaseLabel?: string
|
||||
calledBy?: string
|
||||
}
|
||||
|
||||
interface SubagentSegment {
|
||||
@@ -92,6 +93,7 @@ function parseBlocks(blocks: ContentBlock[], isStreaming: boolean): MessageSegme
|
||||
displayTitle: block.toolCall.displayTitle || formatToolName(block.toolCall.name),
|
||||
status: block.toolCall.status,
|
||||
phaseLabel: block.toolCall.phaseLabel,
|
||||
calledBy: block.toolCall.calledBy,
|
||||
})
|
||||
}
|
||||
break
|
||||
@@ -147,6 +149,7 @@ export function MessageContent({
|
||||
displayTitle={segment.displayTitle}
|
||||
status={segment.status}
|
||||
phaseLabel={segment.phaseLabel}
|
||||
calledBy={segment.calledBy}
|
||||
/>
|
||||
)
|
||||
case 'subagent':
|
||||
|
||||
@@ -97,6 +97,10 @@ export function UserInput({
|
||||
|
||||
onSubmit(fileAttachmentsForApi.length > 0 ? fileAttachmentsForApi : undefined)
|
||||
files.clearAttachedFiles()
|
||||
|
||||
if (textareaRef.current) {
|
||||
textareaRef.current.style.height = 'auto'
|
||||
}
|
||||
}, [onSubmit, files])
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
|
||||
@@ -200,8 +200,30 @@ export function useChat(workspaceId: string, initialChatId?: string): UseChatRet
|
||||
if (restored.length > 0) {
|
||||
setResources(restored)
|
||||
setActiveResourceId(restored[restored.length - 1].id)
|
||||
|
||||
for (const resource of restored) {
|
||||
if (resource.type !== 'workflow') continue
|
||||
const registry = useWorkflowRegistry.getState()
|
||||
if (!registry.workflows[resource.id]) {
|
||||
useWorkflowRegistry.setState((state) => ({
|
||||
workflows: {
|
||||
...state.workflows,
|
||||
[resource.id]: {
|
||||
id: resource.id,
|
||||
name: resource.title,
|
||||
lastModified: new Date(),
|
||||
createdAt: new Date(),
|
||||
color: '#7F2FFF',
|
||||
workspaceId,
|
||||
folderId: null,
|
||||
sortOrder: 0,
|
||||
},
|
||||
},
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [chatHistory])
|
||||
}, [chatHistory, workspaceId])
|
||||
|
||||
const processSSEStream = useCallback(
|
||||
async (reader: ReadableStreamDefaultReader<Uint8Array>, assistantId: string) => {
|
||||
@@ -209,6 +231,7 @@ export function useChat(workspaceId: string, initialChatId?: string): UseChatRet
|
||||
let buffer = ''
|
||||
const blocks: ContentBlock[] = []
|
||||
const toolMap = new Map<string, number>()
|
||||
let activeSubagent: string | undefined
|
||||
let lastTableId: string | null = null
|
||||
let lastWorkflowId: string | null = null
|
||||
let runningText = ''
|
||||
@@ -304,6 +327,7 @@ export function useChat(workspaceId: string, initialChatId?: string): UseChatRet
|
||||
}
|
||||
}
|
||||
|
||||
if (name.endsWith('_respond')) break
|
||||
const ui = parsed.ui || data?.ui
|
||||
if (ui?.hidden) break
|
||||
const displayTitle = ui?.title || ui?.phaseLabel
|
||||
@@ -312,7 +336,14 @@ export function useChat(workspaceId: string, initialChatId?: string): UseChatRet
|
||||
toolMap.set(id, blocks.length)
|
||||
blocks.push({
|
||||
type: 'tool_call',
|
||||
toolCall: { id, name, status: 'executing', displayTitle, phaseLabel },
|
||||
toolCall: {
|
||||
id,
|
||||
name,
|
||||
status: 'executing',
|
||||
displayTitle,
|
||||
phaseLabel,
|
||||
calledBy: activeSubagent,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
const idx = toolMap.get(id)!
|
||||
@@ -424,12 +455,14 @@ export function useChat(workspaceId: string, initialChatId?: string): UseChatRet
|
||||
case 'subagent_start': {
|
||||
const name = parsed.subagent || getPayloadData(parsed)?.agent
|
||||
if (name) {
|
||||
activeSubagent = name
|
||||
blocks.push({ type: 'subagent', content: name })
|
||||
flush()
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'subagent_end': {
|
||||
activeSubagent = undefined
|
||||
flush()
|
||||
break
|
||||
}
|
||||
|
||||
@@ -64,6 +64,7 @@ export type MothershipToolName =
|
||||
| 'research'
|
||||
| 'plan'
|
||||
| 'debug'
|
||||
| 'edit'
|
||||
|
||||
/**
|
||||
* Subagent identifiers dispatched via `subagent_start` SSE events.
|
||||
@@ -102,6 +103,7 @@ export interface ToolCallInfo {
|
||||
status: ToolCallStatus
|
||||
displayTitle?: string
|
||||
phaseLabel?: string
|
||||
calledBy?: string
|
||||
result?: { success: boolean; output?: unknown; error?: string }
|
||||
}
|
||||
|
||||
@@ -180,6 +182,7 @@ export const TOOL_UI_METADATA: Partial<Record<MothershipToolName, ToolUIMetadata
|
||||
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' },
|
||||
}
|
||||
|
||||
export interface SSEPayloadUI {
|
||||
|
||||
@@ -12,6 +12,8 @@ export interface WorkflowBlockProps {
|
||||
isPreview?: boolean
|
||||
/** Whether this block is selected in preview mode */
|
||||
isPreviewSelected?: boolean
|
||||
/** Whether this block is rendered inside an embedded (read-only) workflow view */
|
||||
isEmbedded?: boolean
|
||||
subBlockValues?: Record<string, any>
|
||||
blockState?: any
|
||||
}
|
||||
|
||||
@@ -1248,7 +1248,7 @@ export const WorkflowBlock = memo(function WorkflowBlock({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!data.isPreview && (
|
||||
{!data.isPreview && !data.isEmbedded && (
|
||||
<ActionBar blockId={id} blockType={type} disabled={!userPermissions.canEdit} />
|
||||
)}
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ export function useBlockVisual({
|
||||
isSelected = false,
|
||||
}: UseBlockVisualProps) {
|
||||
const isPreview = data.isPreview ?? false
|
||||
const isEmbedded = data.isEmbedded ?? false
|
||||
const isPreviewSelected = data.isPreviewSelected ?? false
|
||||
|
||||
const currentWorkflow = useCurrentWorkflow()
|
||||
@@ -56,13 +57,13 @@ export function useBlockVisual({
|
||||
const activeTabIsEditor = usePanelStore(
|
||||
useCallback(
|
||||
(state) => {
|
||||
if (isPreview || !isThisBlockInEditor) return false
|
||||
if (isPreview || isEmbedded || !isThisBlockInEditor) return false
|
||||
return state.activeTab === 'editor'
|
||||
},
|
||||
[isPreview, isThisBlockInEditor]
|
||||
[isPreview, isEmbedded, isThisBlockInEditor]
|
||||
)
|
||||
)
|
||||
const isEditorOpen = !isPreview && isThisBlockInEditor && activeTabIsEditor
|
||||
const isEditorOpen = !isPreview && !isEmbedded && isThisBlockInEditor && activeTabIsEditor
|
||||
|
||||
const lastRunPath = useLastRunPath()
|
||||
const runPathStatus = isPreview ? undefined : lastRunPath.get(blockId)
|
||||
@@ -70,10 +71,10 @@ export function useBlockVisual({
|
||||
const setCurrentBlockId = usePanelEditorStore((state) => state.setCurrentBlockId)
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
if (!isPreview) {
|
||||
if (!isPreview && !isEmbedded) {
|
||||
setCurrentBlockId(blockId)
|
||||
}
|
||||
}, [blockId, setCurrentBlockId, isPreview])
|
||||
}, [blockId, setCurrentBlockId, isPreview, isEmbedded])
|
||||
|
||||
const { hasRing, ringClassName: ringStyles } = useMemo(
|
||||
() =>
|
||||
@@ -85,7 +86,7 @@ export function useBlockVisual({
|
||||
diffStatus: isPreview ? undefined : diffStatus,
|
||||
runPathStatus,
|
||||
isPreviewSelection: isPreview && isPreviewSelected,
|
||||
isSelected: isPreview ? false : isSelected,
|
||||
isSelected: isPreview || isEmbedded ? false : isSelected,
|
||||
}),
|
||||
[
|
||||
isExecuting,
|
||||
@@ -95,6 +96,7 @@ export function useBlockVisual({
|
||||
diffStatus,
|
||||
runPathStatus,
|
||||
isPreview,
|
||||
isEmbedded,
|
||||
isPreviewSelected,
|
||||
isSelected,
|
||||
]
|
||||
|
||||
@@ -243,7 +243,7 @@ const WorkflowContent = React.memo(
|
||||
const [isErrorConnectionDrag, setIsErrorConnectionDrag] = useState(false)
|
||||
const selectedIdsRef = useRef<string[] | null>(null)
|
||||
const canvasMode = useCanvasModeStore((state) => state.mode)
|
||||
const isHandMode = embedded ? false : canvasMode === 'hand'
|
||||
const isHandMode = embedded ? true : canvasMode === 'hand'
|
||||
const { handleCanvasMouseDown, selectionProps } = useShiftSelectionLock({ isHandMode })
|
||||
const [oauthModal, setOauthModal] = useState<{
|
||||
provider: OAuthProvider
|
||||
@@ -2299,6 +2299,8 @@ const WorkflowContent = React.memo(
|
||||
|
||||
/** Handles navigation validation and redirects for invalid workflow IDs. */
|
||||
useEffect(() => {
|
||||
if (embedded) return
|
||||
|
||||
// Wait for metadata to finish loading before making navigation decisions
|
||||
if (hydration.phase === 'metadata-loading' || hydration.phase === 'idle') {
|
||||
return
|
||||
@@ -2341,6 +2343,7 @@ const WorkflowContent = React.memo(
|
||||
router.replace(`/workspace/${workflowData.workspaceId}/w/${workflowIdParam}`)
|
||||
}
|
||||
}, [
|
||||
embedded,
|
||||
workflowIdParam,
|
||||
currentWorkflowExists,
|
||||
workflowCount,
|
||||
@@ -2480,6 +2483,7 @@ const WorkflowContent = React.memo(
|
||||
name: block.name,
|
||||
isActive,
|
||||
isPending,
|
||||
...(embedded && { isEmbedded: true }),
|
||||
},
|
||||
// Include dynamic dimensions for container resizing calculations (must match rendered size)
|
||||
// Both note and workflow blocks calculate dimensions deterministically via useBlockDimensions
|
||||
@@ -3862,14 +3866,14 @@ const WorkflowContent = React.memo(
|
||||
onSelectionContextMenu={handleSelectionContextMenu}
|
||||
onPointerMove={handleCanvasPointerMove}
|
||||
onPointerLeave={handleCanvasPointerLeave}
|
||||
elementsSelectable={true}
|
||||
selectionOnDrag={selectionProps.selectionOnDrag}
|
||||
elementsSelectable={!embedded}
|
||||
selectionOnDrag={embedded ? false : selectionProps.selectionOnDrag}
|
||||
selectionMode={SelectionMode.Partial}
|
||||
panOnDrag={selectionProps.panOnDrag}
|
||||
selectionKeyCode={selectionProps.selectionKeyCode}
|
||||
multiSelectionKeyCode={['Meta', 'Control', 'Shift']}
|
||||
nodesConnectable={effectivePermissions.canEdit}
|
||||
nodesDraggable={effectivePermissions.canEdit}
|
||||
panOnDrag={embedded ? true : selectionProps.panOnDrag}
|
||||
selectionKeyCode={embedded ? null : selectionProps.selectionKeyCode}
|
||||
multiSelectionKeyCode={embedded ? null : ['Meta', 'Control', 'Shift']}
|
||||
nodesConnectable={!embedded && effectivePermissions.canEdit}
|
||||
nodesDraggable={!embedded && effectivePermissions.canEdit}
|
||||
draggable={false}
|
||||
noWheelClassName='allow-scroll'
|
||||
edgesFocusable={true}
|
||||
|
||||
@@ -381,6 +381,7 @@ export const Sidebar = memo(function Sidebar() {
|
||||
[fetchedTasks, workspaceId]
|
||||
)
|
||||
|
||||
const [visibleTaskCount, setVisibleTaskCount] = useState(5)
|
||||
const [renamingTaskId, setRenamingTaskId] = useState<string | null>(null)
|
||||
const [renameValue, setRenameValue] = useState('')
|
||||
const renameInputRef = useRef<HTMLInputElement>(null)
|
||||
@@ -803,43 +804,55 @@ export const Sidebar = memo(function Sidebar() {
|
||||
{tasksLoading ? (
|
||||
<SidebarItemSkeleton />
|
||||
) : (
|
||||
tasks.map((task) => {
|
||||
const active = task.id !== 'new' && pathname === task.href
|
||||
const isRenaming = renamingTaskId === task.id
|
||||
<>
|
||||
{tasks.slice(0, visibleTaskCount).map((task) => {
|
||||
const active = task.id !== 'new' && pathname === task.href
|
||||
const isRenaming = renamingTaskId === task.id
|
||||
|
||||
if (isRenaming) {
|
||||
return (
|
||||
<div
|
||||
key={task.id}
|
||||
className='mx-[2px] flex h-[30px] items-center gap-[8px] rounded-[8px] bg-[var(--surface-active)] px-[8px] text-[14px]'
|
||||
>
|
||||
<Blimp className='h-[16px] w-[16px] flex-shrink-0 text-[var(--text-icon)]' />
|
||||
<input
|
||||
ref={renameInputRef}
|
||||
value={renameValue}
|
||||
onChange={(e) => setRenameValue(e.target.value)}
|
||||
onKeyDown={handleRenameKeyDown}
|
||||
onBlur={handleSaveTaskRename}
|
||||
className='min-w-0 flex-1 border-none bg-transparent font-base text-[14px] text-[var(--text-body)] outline-none'
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (isRenaming) {
|
||||
return (
|
||||
<div
|
||||
<Link
|
||||
key={task.id}
|
||||
className='mx-[2px] flex h-[30px] items-center gap-[8px] rounded-[8px] bg-[var(--surface-active)] px-[8px] text-[14px]'
|
||||
href={task.href}
|
||||
className={`mx-[2px] flex h-[30px] items-center gap-[8px] rounded-[8px] px-[8px] text-[14px] hover:bg-[var(--surface-active)] ${active ? 'bg-[var(--surface-active)]' : ''}`}
|
||||
onContextMenu={(e) => handleTaskContextMenu(e, task.href, task.id)}
|
||||
>
|
||||
<Blimp className='h-[16px] w-[16px] flex-shrink-0 text-[var(--text-icon)]' />
|
||||
<input
|
||||
ref={renameInputRef}
|
||||
value={renameValue}
|
||||
onChange={(e) => setRenameValue(e.target.value)}
|
||||
onKeyDown={handleRenameKeyDown}
|
||||
onBlur={handleSaveTaskRename}
|
||||
className='min-w-0 flex-1 border-none bg-transparent font-base text-[14px] text-[var(--text-body)] outline-none'
|
||||
/>
|
||||
</div>
|
||||
<div className='min-w-0 truncate font-[var(--sidebar-font-weight)] text-[var(--text-body)]'>
|
||||
{task.name}
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={task.id}
|
||||
href={task.href}
|
||||
className={`mx-[2px] flex h-[30px] items-center gap-[8px] rounded-[8px] px-[8px] text-[14px] hover:bg-[var(--surface-active)] ${active ? 'bg-[var(--surface-active)]' : ''}`}
|
||||
onContextMenu={(e) => handleTaskContextMenu(e, task.href, task.id)}
|
||||
})}
|
||||
{tasks.length > visibleTaskCount && (
|
||||
<button
|
||||
type='button'
|
||||
onClick={() => setVisibleTaskCount((prev) => prev + 5)}
|
||||
className='mx-[2px] flex h-[30px] items-center gap-[8px] rounded-[8px] px-[8px] text-[14px] text-[var(--text-icon)] hover:bg-[var(--surface-active)]'
|
||||
>
|
||||
<Blimp className='h-[16px] w-[16px] flex-shrink-0 text-[var(--text-icon)]' />
|
||||
<div className='min-w-0 truncate font-[var(--sidebar-font-weight)] text-[var(--text-body)]'>
|
||||
{task.name}
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
})
|
||||
<MoreHorizontal className='h-[16px] w-[16px] flex-shrink-0' />
|
||||
<span className='font-[var(--sidebar-font-weight)]'>See more</span>
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user