diff --git a/apps/sim/app/globals.css b/apps/sim/app/globals.css
index 5da83a32b22..8e63a273ff3 100644
--- a/apps/sim/app/globals.css
+++ b/apps/sim/app/globals.css
@@ -281,6 +281,8 @@
--c-F4F4F4: #f4f4f4;
--c-F5F5F5: #f5f5f5;
+ --c-CFCFCF: #cfcfcf;
+
/* Blues and cyans */
--c-00B0B0: #00b0b0;
--c-264F78: #264f78;
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat.tsx
index c6e2168b037..122ce9cc15f 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat.tsx
@@ -2,7 +2,6 @@
import { type KeyboardEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { AlertCircle, ArrowDownToLine, ArrowUp, MoreVertical, Paperclip, X } from 'lucide-react'
-import { useParams } from 'next/navigation'
import {
Badge,
Button,
@@ -14,6 +13,7 @@ import {
PopoverTrigger,
Trash,
} from '@/components/emcn'
+import { useSession } from '@/lib/auth-client'
import { createLogger } from '@/lib/logs/console/logger'
import {
extractBlockIdFromOutputId,
@@ -21,13 +21,19 @@ import {
parseOutputContentSafely,
} from '@/lib/response-format'
import { cn } from '@/lib/utils'
+import { normalizeInputFormatValue } from '@/lib/workflows/input-format-utils'
+import { StartBlockPath, TriggerUtils } from '@/lib/workflows/triggers'
+import { START_BLOCK_RESERVED_FIELDS } from '@/lib/workflows/types'
import { useScrollManagement } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks'
import { useWorkflowExecution } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution'
import type { BlockLog, ExecutionResult } from '@/executor/types'
import { getChatPosition, useChatStore } from '@/stores/chat/store'
import { useExecutionStore } from '@/stores/execution/store'
+import { useOperationQueue } from '@/stores/operation-queue/store'
import { useTerminalConsoleStore } from '@/stores/terminal'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
+import { useSubBlockStore } from '@/stores/workflows/subblock/store'
+import { useWorkflowStore } from '@/stores/workflows/workflow/store'
import { ChatMessage, OutputSelect } from './components'
import { useChatBoundarySync, useChatDrag, useChatFileUpload, useChatResize } from './hooks'
@@ -124,6 +130,14 @@ const formatOutputContent = (output: any): string => {
return ''
}
+interface StartInputFormatField {
+ id?: string
+ name?: string
+ type?: string
+ value?: unknown
+ collapsed?: boolean
+}
+
/**
* Floating chat modal component
*
@@ -137,9 +151,10 @@ const formatOutputContent = (output: any): string => {
* position across sessions using the floating chat store.
*/
export function Chat() {
- const params = useParams()
- const workspaceId = params.workspaceId as string
const { activeWorkflowId } = useWorkflowRegistry()
+ const blocks = useWorkflowStore((state) => state.blocks)
+ const triggerWorkflowUpdate = useWorkflowStore((state) => state.triggerUpdate)
+ const setSubBlockValue = useSubBlockStore((state) => state.setValue)
// Chat state (UI and messages from unified store)
const {
@@ -164,6 +179,8 @@ export function Chat() {
const { entries } = useTerminalConsoleStore()
const { isExecuting } = useExecutionStore()
const { handleRunWorkflow } = useWorkflowExecution()
+ const { data: session } = useSession()
+ const { addToQueue } = useOperationQueue()
// Local state
const [chatMessage, setChatMessage] = useState('')
@@ -190,6 +207,71 @@ export function Chat() {
handleDrop,
} = useChatFileUpload()
+ /**
+ * Resolves the unified start block for chat execution, if available.
+ */
+ const startBlockCandidate = useMemo(() => {
+ if (!activeWorkflowId) {
+ return null
+ }
+
+ if (!blocks || Object.keys(blocks).length === 0) {
+ return null
+ }
+
+ const candidate = TriggerUtils.findStartBlock(blocks, 'chat')
+ if (!candidate || candidate.path !== StartBlockPath.UNIFIED) {
+ return null
+ }
+
+ return candidate
+ }, [activeWorkflowId, blocks])
+
+ const startBlockId = startBlockCandidate?.blockId ?? null
+
+ /**
+ * Reads the current input format for the unified start block from the subblock store,
+ * falling back to the workflow store if no explicit value is stored yet.
+ */
+ const startBlockInputFormat = useSubBlockStore((state) => {
+ if (!activeWorkflowId || !startBlockId) {
+ return null
+ }
+
+ const workflowValues = state.workflowValues[activeWorkflowId]
+ const fromStore = workflowValues?.[startBlockId]?.inputFormat
+ if (fromStore !== undefined && fromStore !== null) {
+ return fromStore
+ }
+
+ const startBlock = blocks[startBlockId]
+ return startBlock?.subBlocks?.inputFormat?.value ?? null
+ })
+
+ /**
+ * Determines which reserved start inputs are missing from the input format.
+ */
+ const missingStartReservedFields = useMemo(() => {
+ if (!startBlockId) {
+ return START_BLOCK_RESERVED_FIELDS
+ }
+
+ const normalizedFields = normalizeInputFormatValue(startBlockInputFormat)
+ const existingNames = new Set(
+ normalizedFields
+ .map((field) => field.name)
+ .filter((name): name is string => typeof name === 'string' && name.trim() !== '')
+ .map((name) => name.trim().toLowerCase())
+ )
+
+ return START_BLOCK_RESERVED_FIELDS.filter(
+ (fieldName) => !existingNames.has(fieldName.toLowerCase())
+ )
+ }, [startBlockId, startBlockInputFormat])
+
+ const shouldShowConfigureStartInputsButton =
+ Boolean(startBlockId) && missingStartReservedFields.length > 0
+
// Get actual position (default if not set)
const actualPosition = useMemo(
() => getChatPosition(chatPosition, chatWidth, chatHeight),
@@ -564,7 +646,67 @@ export function Chat() {
setIsChatOpen(false)
}, [setIsChatOpen])
- // Don't render if not open
+ /**
+ * Adds any missing reserved inputs (input, conversationId, files) to the unified start block.
+ */
+ const handleConfigureStartInputs = useCallback(() => {
+ if (!activeWorkflowId || !startBlockId) {
+ logger.warn('Cannot configure start inputs: missing active workflow ID or start block ID')
+ return
+ }
+
+ try {
+ const normalizedExisting = normalizeInputFormatValue(startBlockInputFormat)
+
+ const newReservedFields: StartInputFormatField[] = missingStartReservedFields.map(
+ (fieldName) => {
+ const defaultType = fieldName === 'files' ? 'files' : 'string'
+
+ return {
+ id: crypto.randomUUID(),
+ name: fieldName,
+ type: defaultType,
+ value: '',
+ collapsed: false,
+ }
+ }
+ )
+
+ const updatedFields: StartInputFormatField[] = [...newReservedFields, ...normalizedExisting]
+
+ setSubBlockValue(startBlockId, 'inputFormat', updatedFields)
+
+ const userId = session?.user?.id || 'unknown'
+ addToQueue({
+ id: crypto.randomUUID(),
+ operation: {
+ operation: 'subblock-update',
+ target: 'subblock',
+ payload: {
+ blockId: startBlockId,
+ subblockId: 'inputFormat',
+ value: updatedFields,
+ },
+ },
+ workflowId: activeWorkflowId,
+ userId,
+ })
+
+ triggerWorkflowUpdate()
+ } catch (error) {
+ logger.error('Failed to configure start block reserved inputs', error)
+ }
+ }, [
+ activeWorkflowId,
+ missingStartReservedFields,
+ setSubBlockValue,
+ startBlockId,
+ startBlockInputFormat,
+ triggerWorkflowUpdate,
+ session,
+ addToQueue,
+ ])
+
if (!isChatOpen) return null
return (
@@ -583,17 +725,32 @@ export function Chat() {
>
{/* Header with drag handle */}
-
-
- Chat
-
-
+
+ Chat
+
+
+ {/* Start inputs button and output selector - with max-width to prevent overflow */}
+
e.stopPropagation()}
+ >
+ {shouldShowConfigureStartInputsButton && (
+
{
+ e.stopPropagation()
+ handleConfigureStartInputs()
+ }}
+ >
+ Add inputs
+
+ )}
- {/* Output selector - centered with mx-auto */}
-
e.stopPropagation()}>
-
+
{/* More menu with actions */}
@@ -628,22 +785,22 @@ export function Chat() {
{
e.stopPropagation()
- if (activeWorkflowId) clearChat(activeWorkflowId)
+ if (activeWorkflowId) exportChatCSV(activeWorkflowId)
}}
- disabled={messages.length === 0}
+ disabled={workflowMessages.length === 0}
>
-
- Clear
+
+ Download
{
e.stopPropagation()
- if (activeWorkflowId) exportChatCSV(activeWorkflowId)
+ if (activeWorkflowId) clearChat(activeWorkflowId)
}}
- disabled={messages.length === 0}
+ disabled={workflowMessages.length === 0}
>
-
- Download
+
+ Clear
@@ -662,7 +819,7 @@ export function Chat() {
{workflowMessages.length === 0 ? (
- No messages yet
+ Workflow input: {''}
) : (
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/hooks/use-chat-resize.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/hooks/use-chat-resize.ts
index e9050264d81..08a3e17d27d 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/hooks/use-chat-resize.ts
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/hooks/use-chat-resize.ts
@@ -178,16 +178,11 @@ export function useChatResize({
(e: MouseEvent) => {
if (!isResizingRef.current || !activeDirectionRef.current) return
- const deltaX = e.clientX - resizeStartRef.current.x
- const deltaY = e.clientY - resizeStartRef.current.y
+ let deltaX = e.clientX - resizeStartRef.current.x
+ let deltaY = e.clientY - resizeStartRef.current.y
const initial = initialStateRef.current
const direction = activeDirectionRef.current
- let newX = initial.x
- let newY = initial.y
- let newWidth = initial.width
- let newHeight = initial.height
-
// Get layout bounds
const sidebarWidth = Number.parseInt(
getComputedStyle(document.documentElement).getPropertyValue('--sidebar-width') || '0'
@@ -199,6 +194,56 @@ export function useChatResize({
getComputedStyle(document.documentElement).getPropertyValue('--terminal-height') || '0'
)
+ // Clamp vertical drag when resizing from the top so the chat does not grow downward
+ // after its top edge hits the top of the viewport.
+ if (direction === 'top' || direction === 'top-left' || direction === 'top-right') {
+ // newY = initial.y + deltaY should never be less than 0
+ const maxUpwardDelta = initial.y
+ if (deltaY < -maxUpwardDelta) {
+ deltaY = -maxUpwardDelta
+ }
+ }
+
+ // Clamp vertical drag when resizing from the bottom so the chat does not grow upward
+ // after its bottom edge hits the top of the terminal.
+ if (direction === 'bottom' || direction === 'bottom-left' || direction === 'bottom-right') {
+ const maxBottom = window.innerHeight - terminalHeight
+ const initialBottom = initial.y + initial.height
+ const maxDeltaY = maxBottom - initialBottom
+
+ if (deltaY > maxDeltaY) {
+ deltaY = maxDeltaY
+ }
+ }
+
+ // Clamp horizontal drag when resizing from the left so the chat does not grow to the right
+ // after its left edge hits the sidebar.
+ if (direction === 'left' || direction === 'top-left' || direction === 'bottom-left') {
+ const minLeft = sidebarWidth
+ const minDeltaX = minLeft - initial.x
+
+ if (deltaX < minDeltaX) {
+ deltaX = minDeltaX
+ }
+ }
+
+ // Clamp horizontal drag when resizing from the right so the chat does not grow to the left
+ // after its right edge hits the panel.
+ if (direction === 'right' || direction === 'top-right' || direction === 'bottom-right') {
+ const maxRight = window.innerWidth - panelWidth
+ const initialRight = initial.x + initial.width
+ const maxDeltaX = maxRight - initialRight
+
+ if (deltaX > maxDeltaX) {
+ deltaX = maxDeltaX
+ }
+ }
+
+ let newX = initial.x
+ let newY = initial.y
+ let newWidth = initial.width
+ let newHeight = initial.height
+
// Calculate new dimensions based on resize direction
switch (direction) {
// Corners
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/panel-new.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/panel-new.tsx
index 7ce096f7111..20f93a9ca1e 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/panel-new.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/panel-new.tsx
@@ -1,7 +1,7 @@
'use client'
import { useCallback, useEffect, useRef, useState } from 'react'
-import { ArrowDown, Braces, Square } from 'lucide-react'
+import { Braces, Square } from 'lucide-react'
import { useParams, useRouter } from 'next/navigation'
import {
BubbleChatPreview,
@@ -270,14 +270,6 @@ export function Panel() {
workspaceId,
])
- /**
- * Handles triggering file input for workflow import
- */
- const handleImportWorkflow = useCallback(() => {
- setIsMenuOpen(false)
- fileInputRef.current?.click()
- }, [])
-
// Compute run button state
const canRun = userPermissions.canRead // Running only requires read permissions
const isLoadingPermissions = userPermissions.isLoading
@@ -349,13 +341,6 @@ export function Panel() {
Export workflow
-
-
- Import workflow
-
-
- {/* Hidden file input for workflow import */}
-
>
)
}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/terminal.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/terminal.tsx
index 2699a823fe6..6f8cbdff4ea 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/terminal.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/terminal.tsx
@@ -721,7 +721,8 @@ export function Terminal() {
align='start'
sideOffset={4}
onClick={(e) => e.stopPropagation()}
- style={{ minWidth: '120px', maxWidth: '120px' }}
+ minWidth={120}
+ maxWidth={200}
>
{uniqueBlocks.map((block, index) => {
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx
index cdced8dd364..7938954b5f0 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx
@@ -794,7 +794,7 @@ export const WorkflowBlock = memo(function WorkflowBlock({
ref={contentRef}
onClick={handleClick}
className={cn(
- 'relative z-[20] w-[250px] cursor-default select-none rounded-[8px] bg-[var(--surface-2)]'
+ 'relative z-[20] w-[250px] cursor-default select-none rounded-[8px] border border-[var(--border)] bg-[var(--surface-2)]'
)}
>
{isPending && (
diff --git a/apps/sim/lib/workflows/block-outputs.ts b/apps/sim/lib/workflows/block-outputs.ts
index 01c4d3cdf83..3027e113ff9 100644
--- a/apps/sim/lib/workflows/block-outputs.ts
+++ b/apps/sim/lib/workflows/block-outputs.ts
@@ -1,3 +1,4 @@
+import { normalizeInputFormatValue } from '@/lib/workflows/input-format-utils'
import { classifyStartBlockType, StartBlockPath, TRIGGER_TYPES } from '@/lib/workflows/triggers'
import {
type InputFormatField,
@@ -23,24 +24,6 @@ const UNIFIED_START_OUTPUTS: OutputDefinition = {
files: { type: 'files', description: 'User uploaded files' },
}
-function normalizeInputFormatValue(inputFormatValue: any): InputFormatField[] {
- if (
- inputFormatValue === null ||
- inputFormatValue === undefined ||
- (Array.isArray(inputFormatValue) && inputFormatValue.length === 0)
- ) {
- return []
- }
-
- if (!Array.isArray(inputFormatValue)) {
- return []
- }
-
- return inputFormatValue.filter(
- (field) => field && typeof field === 'object' && field.name && field.name.trim() !== ''
- )
-}
-
function applyInputFormatFields(
inputFormat: InputFormatField[],
outputs: OutputDefinition
diff --git a/apps/sim/lib/workflows/input-format-utils.ts b/apps/sim/lib/workflows/input-format-utils.ts
new file mode 100644
index 00000000000..fec752d89ff
--- /dev/null
+++ b/apps/sim/lib/workflows/input-format-utils.ts
@@ -0,0 +1,39 @@
+import type { InputFormatField } from '@/lib/workflows/types'
+
+/**
+ * Normalizes an input format value into a list of valid fields.
+ *
+ * Filters out:
+ * - null or undefined values
+ * - Empty arrays
+ * - Non-array values
+ * - Fields without names
+ * - Fields with empty or whitespace-only names
+ *
+ * @param inputFormatValue - Raw input format value from subblock state
+ * @returns Array of validated input format fields
+ */
+export function normalizeInputFormatValue(inputFormatValue: unknown): InputFormatField[] {
+ // Handle null, undefined, and empty arrays
+ if (
+ inputFormatValue === null ||
+ inputFormatValue === undefined ||
+ (Array.isArray(inputFormatValue) && inputFormatValue.length === 0)
+ ) {
+ return []
+ }
+
+ // Handle non-array values
+ if (!Array.isArray(inputFormatValue)) {
+ return []
+ }
+
+ // Filter valid fields
+ return inputFormatValue.filter(
+ (field): field is InputFormatField =>
+ field &&
+ typeof field === 'object' &&
+ typeof field.name === 'string' &&
+ field.name.trim() !== ''
+ )
+}
diff --git a/apps/sim/stores/chat/store.ts b/apps/sim/stores/chat/store.ts
index f2ef913af7c..b1fcff4a6d8 100644
--- a/apps/sim/stores/chat/store.ts
+++ b/apps/sim/stores/chat/store.ts
@@ -13,7 +13,7 @@ const MAX_MESSAGES = 50
/**
* Floating chat dimensions
*/
-const DEFAULT_WIDTH = 250
+const DEFAULT_WIDTH = 330
const DEFAULT_HEIGHT = 286
/**