diff --git a/apps/sim/app/_styles/globals.css b/apps/sim/app/_styles/globals.css index 254534f2891..ddb0d181485 100644 --- a/apps/sim/app/_styles/globals.css +++ b/apps/sim/app/_styles/globals.css @@ -677,17 +677,26 @@ input[type="search"]::-ms-clear { * Panel tab visibility and styling to prevent hydration flash */ html[data-panel-active-tab="copilot"] .panel-container [data-tab-content="toolbar"], - html[data-panel-active-tab="copilot"] .panel-container [data-tab-content="editor"] { + html[data-panel-active-tab="copilot"] .panel-container [data-tab-content="editor"], + html[data-panel-active-tab="copilot"] .panel-container [data-tab-content="logs"] { display: none !important; } html[data-panel-active-tab="toolbar"] .panel-container [data-tab-content="copilot"], - html[data-panel-active-tab="toolbar"] .panel-container [data-tab-content="editor"] { + html[data-panel-active-tab="toolbar"] .panel-container [data-tab-content="editor"], + html[data-panel-active-tab="toolbar"] .panel-container [data-tab-content="logs"] { display: none !important; } html[data-panel-active-tab="editor"] .panel-container [data-tab-content="copilot"], - html[data-panel-active-tab="editor"] .panel-container [data-tab-content="toolbar"] { + html[data-panel-active-tab="editor"] .panel-container [data-tab-content="toolbar"], + html[data-panel-active-tab="editor"] .panel-container [data-tab-content="logs"] { + display: none !important; + } + + html[data-panel-active-tab="logs"] .panel-container [data-tab-content="copilot"], + html[data-panel-active-tab="logs"] .panel-container [data-tab-content="toolbar"], + html[data-panel-active-tab="logs"] .panel-container [data-tab-content="editor"] { display: none !important; } @@ -696,7 +705,8 @@ input[type="search"]::-ms-clear { color: var(--text-primary) !important; } html[data-panel-active-tab="copilot"] .panel-container [data-tab-button="toolbar"], - html[data-panel-active-tab="copilot"] .panel-container [data-tab-button="editor"] { + html[data-panel-active-tab="copilot"] .panel-container [data-tab-button="editor"], + html[data-panel-active-tab="copilot"] .panel-container [data-tab-button="logs"] { background-color: transparent !important; color: var(--text-tertiary) !important; } @@ -706,7 +716,8 @@ input[type="search"]::-ms-clear { color: var(--text-primary) !important; } html[data-panel-active-tab="toolbar"] .panel-container [data-tab-button="copilot"], - html[data-panel-active-tab="toolbar"] .panel-container [data-tab-button="editor"] { + html[data-panel-active-tab="toolbar"] .panel-container [data-tab-button="editor"], + html[data-panel-active-tab="toolbar"] .panel-container [data-tab-button="logs"] { background-color: transparent !important; color: var(--text-tertiary) !important; } @@ -716,7 +727,19 @@ input[type="search"]::-ms-clear { color: var(--text-primary) !important; } html[data-panel-active-tab="editor"] .panel-container [data-tab-button="copilot"], - html[data-panel-active-tab="editor"] .panel-container [data-tab-button="toolbar"] { + html[data-panel-active-tab="editor"] .panel-container [data-tab-button="toolbar"], + html[data-panel-active-tab="editor"] .panel-container [data-tab-button="logs"] { + background-color: transparent !important; + color: var(--text-tertiary) !important; + } + + html[data-panel-active-tab="logs"] .panel-container [data-tab-button="logs"] { + background-color: var(--border-1) !important; + color: var(--text-primary) !important; + } + html[data-panel-active-tab="logs"] .panel-container [data-tab-button="copilot"], + html[data-panel-active-tab="logs"] .panel-container [data-tab-button="toolbar"], + html[data-panel-active-tab="logs"] .panel-container [data-tab-button="editor"] { background-color: transparent !important; color: var(--text-tertiary) !important; } @@ -968,3 +991,7 @@ input[type="search"]::-ms-clear { .react-flow__node[data-parent-node-id] .react-flow__handle { z-index: 30; } + +.react-flow__panel { + margin: 0 !important; +} diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/credentials/credentials-manager.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/credentials/credentials-manager.tsx index 21ae97ca049..639a26e7bb8 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/credentials/credentials-manager.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/credentials/credentials-manager.tsx @@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { createLogger } from '@sim/logger' -import { Check, Clipboard, Key, Search } from 'lucide-react' +import { Check, Clipboard, Info, Key, Search, Shield, UserPlus } from 'lucide-react' import { useParams, useRouter } from 'next/navigation' import { Avatar, @@ -1188,44 +1188,78 @@ export function CredentialsManager() { )} -
- +
+ {/* Header */} +
+
+ +
+
+
+

+ Access Control +

+ + {activeMembers.length} {activeMembers.length === 1 ? 'member' : 'members'} + +
+

+ Only workspace members listed below can view and use this secret in their + workflows. Admins can manage access; members can only use the secret. +

+
+
+ {/* Member list */} {membersLoading ? ( -
- - +
+
+ +
+ + +
+ +
+
+ +
+ + +
+ +
) : ( -
- {activeMembers.map((member) => ( +
+ {activeMembers.map((member, index) => (
0 ? 'border-[var(--border)] border-t' : '' + }`} > -
- - - {(member.userName || member.userEmail || '?').charAt(0).toUpperCase()} - - -
-

- {member.userName || member.userEmail || member.userId} -

-

- {member.userEmail || member.userId} -

-
+ + + {(member.userName || member.userEmail || '?').charAt(0).toUpperCase()} + + +
+

+ {member.userName || member.userEmail || member.userId} +

+

+ {member.userEmail || member.userId} +

{isSelectedAdmin ? ( - <> +
({ value: option.value, @@ -1250,55 +1284,85 @@ export function CredentialsManager() { variant='ghost' onClick={() => handleRemoveMember(member.userId)} disabled={member.role === 'admin' && adminMemberCount <= 1} - className='w-full justify-end' + className='h-7 px-2 text-[var(--text-tertiary)] text-caption hover-hover:text-[var(--text-error)]' > Remove - +
) : ( - <> - {member.role} -
- + + {member.role === 'admin' ? 'Admin' : 'Member'} + )}
))} + + {/* Add member row */} {isSelectedAdmin && ( -
- option.value === memberUserId) - ?.label || '' - } - selectedValue={memberUserId} - onChange={setMemberUserId} - placeholder='Add member...' - searchable - searchPlaceholder='Search members...' - size='sm' - /> - ({ - value: option.value, - label: option.label, - }))} - value={ - ROLE_OPTIONS.find((option) => option.value === memberRole)?.label || '' - } - selectedValue={memberRole} - onChange={(value) => setMemberRole(value as WorkspaceCredentialRole)} - placeholder='Role' - size='sm' - /> - +
+
+ +

+ Grant access to a workspace member +

+
+
+
+ option.value === memberUserId) + ?.label || '' + } + selectedValue={memberUserId} + onChange={setMemberUserId} + placeholder='Select workspace member...' + searchable + searchPlaceholder='Search workspace members...' + emptyMessage='No workspace members available. Invite members to the workspace first.' + size='sm' + /> +
+
+ ({ + value: option.value, + label: option.label, + }))} + value={ + ROLE_OPTIONS.find((option) => option.value === memberRole)?.label || + '' + } + selectedValue={memberRole} + onChange={(value) => setMemberRole(value as WorkspaceCredentialRole)} + placeholder='Role' + size='sm' + /> +
+ +
+

+ + Only members of this workspace appear here. To add someone new, invite + them to the workspace first. +

+
+ )} + + {/* Non-admin notice */} + {!isSelectedAdmin && ( +
+ +

+ Only admins of this secret can manage access control. +

)}
@@ -1307,7 +1371,7 @@ export function CredentialsManager() {
-
+
+
+
+ ) +}) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/index.ts index 7c32706966d..9cc21cde6e3 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/index.ts @@ -1,6 +1,7 @@ export { BlockMenu } from './block-menu' export { CanvasMenu } from './canvas-menu' export { CommandList } from './command-list/command-list' +export { CopilotInput } from './copilot-input/copilot-input' export { Cursors } from './cursors/cursors' export { DiffControls } from './diff-controls/diff-controls' export { ErrorBoundary } from './error/index' @@ -9,6 +10,8 @@ export { Panel } from './panel/panel' export { SubflowNodeComponent } from './subflows/subflow-node' export { Terminal } from './terminal/terminal' export { WandPromptBar } from './wand-prompt-bar/wand-prompt-bar' +export { WorkflowActions } from './workflow-actions' export { WorkflowBlock } from './workflow-block/workflow-block' export { WorkflowControls } from './workflow-controls' export { WorkflowEdge } from './workflow-edge/workflow-edge' +export { WorkflowToolbar } from './workflow-toolbar' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/general/components/versions.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/general/components/versions.tsx index 82c09c5817c..825a1aa12ad 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/general/components/versions.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/general/components/versions.tsx @@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from 'react' import clsx from 'clsx' -import { FileText, MoreVertical, Pencil, RotateCcw, SendToBack } from 'lucide-react' +import { MoreVertical, NotepadText, Pencil, RotateCcw, SendToBack } from 'lucide-react' import { Button, Popover, @@ -304,7 +304,7 @@ export function Versions({ )} onClick={() => handleOpenDescriptionModal(v.version)} > - + @@ -329,10 +329,7 @@ export function Versions({ Rename - handleOpenDescriptionModal(v.version)}> - - {v.description ? 'Edit description' : 'Add description'} - + {!v.isActive && ( handlePromote(v.version)}> diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx index 4c5ecbbc570..316209f018b 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx @@ -2,47 +2,28 @@ import { memo, useCallback, useEffect, useRef, useState } from 'react' import { createLogger } from '@sim/logger' -import { History, Plus, Square } from 'lucide-react' -import { useParams, useRouter } from 'next/navigation' +import { Bot, History, Pencil, Plus, TerminalSquare, Wrench } from 'lucide-react' +import { useParams } from 'next/navigation' import { useShallow } from 'zustand/react/shallow' import { - BubbleChatClose, - BubbleChatPreview, Button, - Copy, - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, - Layout, - Modal, - ModalBody, - ModalContent, - ModalFooter, - ModalHeader, - MoreHorizontal, - Play, Popover, PopoverContent, PopoverItem, PopoverScrollArea, PopoverSection, PopoverTrigger, + Tooltip, Trash, } from '@/components/emcn' -import { Lock, Unlock, Upload } from '@/components/emcn/icons' -import { VariableIcon } from '@/components/icons' import { useSession } from '@/lib/auth/auth-client' -import { generateWorkflowJson } from '@/lib/workflows/operations/import-export' import { ConversationListItem } from '@/app/workspace/[workspaceId]/components' import { MothershipChat } from '@/app/workspace/[workspaceId]/home/components' import { getWorkflowCopilotUseChatOptions, useChat } from '@/app/workspace/[workspaceId]/home/hooks' import type { FileAttachmentForApi } from '@/app/workspace/[workspaceId]/home/types' import { useRegisterGlobalCommands } from '@/app/workspace/[workspaceId]/providers/global-commands-provider' -import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider' import { createCommands } from '@/app/workspace/[workspaceId]/utils/commands-utils' import { - Deploy, Editor, Toolbar, } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components' @@ -50,30 +31,20 @@ import { usePanelResize, useUsageLimits, } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/hooks' +import { Terminal } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/terminal' import { Variables } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/variables/variables' -import { useAutoLayout } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-auto-layout' -import { useCurrentWorkflow } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-current-workflow' import { useWorkflowExecution } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution' -import { getWorkflowLockToggleIds } from '@/app/workspace/[workspaceId]/w/[workflowId]/utils' -import { useDeleteWorkflow, useImportWorkflow } from '@/app/workspace/[workspaceId]/w/hooks' -import { useDuplicateWorkflowMutation, useWorkflowMap } from '@/hooks/queries/workflows' -import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow' import { usePermissionConfig } from '@/hooks/use-permission-config' import { useSettingsNavigation } from '@/hooks/use-settings-navigation' -import { useChatStore } from '@/stores/chat/store' -import { useNotificationStore } from '@/stores/notifications/store' import type { ChatContext, PanelTab } from '@/stores/panel' import { usePanelStore } from '@/stores/panel' -import { useVariablesModalStore } from '@/stores/variables/modal' -import { useVariablesStore } from '@/stores/variables/store' import { useWorkflowDiffStore } from '@/stores/workflow-diff/store' import { captureBaselineSnapshot } from '@/stores/workflow-diff/utils' -import { getWorkflowWithValues } from '@/stores/workflows' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' -import { useWorkflowStore } from '@/stores/workflows/workflow/store' import type { WorkflowState } from '@/stores/workflows/workflow/types' const logger = createLogger('Panel') + /** * Panel component with resizable width and tab navigation that persists across page refreshes. * @@ -97,19 +68,31 @@ interface PanelProps { } export const Panel = memo(function Panel({ workspaceId: propWorkspaceId }: PanelProps = {}) { - const router = useRouter() const params = useParams() const workspaceId = propWorkspaceId ?? (params.workspaceId as string) const panelRef = useRef(null) - const fileInputRef = useRef(null) - const { activeTab, setActiveTab, panelWidth, _hasHydrated, setHasHydrated } = usePanelStore( + const { + activeTab, + setActiveTab, + panelWidth, + isPanelOpen, + setIsPanelOpen, + _hasHydrated, + setHasHydrated, + pendingCopilotMessage, + setPendingCopilotMessage, + } = usePanelStore( useShallow((state) => ({ activeTab: state.activeTab, setActiveTab: state.setActiveTab, panelWidth: state.panelWidth, + isPanelOpen: state.isPanelOpen, + setIsPanelOpen: state.setIsPanelOpen, _hasHydrated: state._hasHydrated, setHasHydrated: state.setHasHydrated, + pendingCopilotMessage: state.pendingCopilotMessage, + setPendingCopilotMessage: state.setPendingCopilotMessage, })) ) const toolbarRef = useRef<{ @@ -117,19 +100,8 @@ export const Panel = memo(function Panel({ workspaceId: propWorkspaceId }: Panel } | null>(null) const { data: session } = useSession() - // State - const [isMenuOpen, setIsMenuOpen] = useState(false) - const [isAutoLayouting, setIsAutoLayouting] = useState(false) - const [isExporting, setIsExporting] = useState(false) - const [isDuplicating, setIsDuplicating] = useState(false) - const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false) - // Hooks - const userPermissions = useUserPermissionsContext() const { config: permissionConfig } = usePermissionConfig() - const { isImporting, handleFileChange } = useImportWorkflow({ workspaceId }) - const duplicateWorkflowMutation = useDuplicateWorkflowMutation() - const { data: workflows = {} } = useWorkflowMap(workspaceId) const { activeWorkflowId, hydration } = useWorkflowRegistry( useShallow((state) => ({ activeWorkflowId: state.activeWorkflowId, @@ -137,31 +109,8 @@ export const Panel = memo(function Panel({ workspaceId: propWorkspaceId }: Panel })) ) const isRegistryLoading = hydration.phase === 'idle' || hydration.phase === 'state-loading' - const { handleAutoLayout: autoLayoutWithFitView } = useAutoLayout(activeWorkflowId || null) - - // Check for locked blocks (disables auto-layout) - const hasLockedBlocks = useWorkflowStore((state) => - Object.values(state.blocks).some((block) => block.locked) - ) - - const allBlocksLocked = useWorkflowStore((state) => { - const blockList = Object.values(state.blocks) - return blockList.length > 0 && blockList.every((block) => block.locked) - }) - - const hasBlocks = useWorkflowStore((state) => Object.keys(state.blocks).length > 0) - - const { collaborativeBatchToggleLocked } = useCollaborativeWorkflow() const { navigateToSettings } = useSettingsNavigation() - // Delete workflow hook - const { isDeleting, handleDeleteWorkflow } = useDeleteWorkflow({ - workspaceId, - workflowIds: activeWorkflowId || '', - isActive: true, - onSuccess: () => setIsDeleteModalOpen(false), - }) - // Usage limits hook const { usageExceeded } = useUsageLimits({ context: 'user', @@ -174,23 +123,14 @@ export const Panel = memo(function Panel({ workspaceId: propWorkspaceId }: Panel // Panel resize hook const { handleMouseDown } = usePanelResize() - /** - * Opens subscription settings modal - */ const openSubscriptionSettings = () => { navigateToSettings({ section: 'subscription' }) } - /** - * Cancels the currently executing workflow - */ const cancelWorkflow = useCallback(async () => { await handleCancelExecution() }, [handleCancelExecution]) - /** - * Runs the workflow with usage limit check - */ const runWorkflow = useCallback(async () => { if (usageExceeded) { openSubscriptionSettings() @@ -200,22 +140,6 @@ export const Panel = memo(function Panel({ workspaceId: propWorkspaceId }: Panel }, [usageExceeded, handleRunWorkflow]) // Chat state - const { isChatOpen, setIsChatOpen } = useChatStore( - useShallow((state) => ({ - isChatOpen: state.isChatOpen, - setIsChatOpen: state.setIsChatOpen, - })) - ) - const { isOpen: isVariablesOpen, setIsOpen: setVariablesOpen } = useVariablesModalStore( - useShallow((state) => ({ - isOpen: state.isOpen, - setIsOpen: state.setIsOpen, - })) - ) - - const currentWorkflow = activeWorkflowId ? workflows[activeWorkflowId] : null - const { isSnapshotView } = useCurrentWorkflow() - const [copilotChatId, setCopilotChatId] = useState(undefined) const [copilotChatTitle, setCopilotChatTitle] = useState(null) const [copilotChatList, setCopilotChatList] = useState< @@ -403,6 +327,14 @@ export const Panel = memo(function Panel({ workspaceId: propWorkspaceId }: Panel [copilotSendMessage] ) + useEffect(() => { + if (pendingCopilotMessage && isPanelOpen && activeTab === 'copilot') { + const message = pendingCopilotMessage + setPendingCopilotMessage(null) + handleCopilotSubmit(message) + } + }, [pendingCopilotMessage, isPanelOpen, activeTab, setPendingCopilotMessage, handleCopilotSubmit]) + /** * Mark hydration as complete on mount * This allows React to take over visibility control from CSS @@ -423,153 +355,27 @@ export const Panel = memo(function Panel({ workspaceId: propWorkspaceId }: Panel }, [setActiveTab, copilotSendMessage]) /** - * Handles tab click events - */ - const handleTabClick = (tab: PanelTab) => { - setActiveTab(tab) - } - - /** - * Downloads a file with the given content - */ - const downloadFile = useCallback((content: string, filename: string, mimeType: string) => { - try { - const blob = new Blob([content], { type: mimeType }) - const url = URL.createObjectURL(blob) - const a = document.createElement('a') - a.href = url - a.download = filename - document.body.appendChild(a) - a.click() - document.body.removeChild(a) - URL.revokeObjectURL(url) - } catch (error) { - logger.error('Failed to download file:', error) - } - }, []) - - /** - * Handles auto-layout of workflow blocks - */ - const handleAutoLayout = useCallback(async () => { - if (isExecuting || !userPermissions.canEdit || isAutoLayouting) { - return - } - - setIsAutoLayouting(true) - try { - const result = await autoLayoutWithFitView() - if (!result.success && result.error) { - useNotificationStore.getState().addNotification({ - level: 'info', - message: result.error, - workflowId: activeWorkflowId || undefined, - }) - } - } finally { - setIsAutoLayouting(false) - } - }, [ - isExecuting, - userPermissions.canEdit, - isAutoLayouting, - autoLayoutWithFitView, - activeWorkflowId, - ]) - - /** - * Handles exporting workflow as JSON + * Context-aware tab switching: + * When a workflow run finishes, auto-switch to the Logs tab so the user + * can immediately see results — unless they're actively editing a block. */ - const handleExportJson = useCallback(async () => { - if (!currentWorkflow || !activeWorkflowId) { - logger.warn('No active workflow to export') - return - } - - setIsExporting(true) - try { - const workflow = getWorkflowWithValues(activeWorkflowId, workspaceId) - - if (!workflow || !workflow.state) { - throw new Error('No workflow state found') + const wasExecutingRef = useRef(false) + useEffect(() => { + if (wasExecutingRef.current && !isExecuting) { + if (activeTab !== 'editor' && activeTab !== 'copilot') { + setActiveTab('logs') + if (!isPanelOpen) setIsPanelOpen(true) } - - const workflowVariables = useVariablesStore - .getState() - .getVariablesByWorkflowId(activeWorkflowId) - - const jsonContent = generateWorkflowJson(workflow.state, { - workflowId: activeWorkflowId, - name: currentWorkflow.name, - description: currentWorkflow.description, - variables: workflowVariables.map((v) => ({ - id: v.id, - name: v.name, - type: v.type, - value: v.value, - })), - }) - - const filename = `${currentWorkflow.name.replace(/[^a-z0-9]/gi, '-')}.json` - downloadFile(jsonContent, filename, 'application/json') - logger.info('Workflow exported as JSON') - } catch (error) { - logger.error('Failed to export workflow as JSON:', error) - } finally { - setIsExporting(false) - setIsMenuOpen(false) - } - }, [currentWorkflow, activeWorkflowId, downloadFile]) - - /** - * Handles duplicating the current workflow - */ - const handleDuplicateWorkflow = useCallback(async () => { - if (!activeWorkflowId || !userPermissions.canEdit || isDuplicating) { - return } + wasExecutingRef.current = isExecuting + }, [isExecuting, activeTab, isPanelOpen, setActiveTab, setIsPanelOpen]) - const sourceWorkflow = workflows[activeWorkflowId] - if (!sourceWorkflow) return - - setIsDuplicating(true) - try { - const result = await duplicateWorkflowMutation.mutateAsync({ - workspaceId, - sourceId: activeWorkflowId, - name: `${sourceWorkflow.name} (Copy)`, - description: sourceWorkflow.description, - color: sourceWorkflow.color ?? '', - folderId: sourceWorkflow.folderId, - }) - if (result?.id) { - router.push(`/workspace/${workspaceId}/w/${result.id}`) - } - } catch (error) { - logger.error('Error duplicating workflow:', error) - } finally { - setIsDuplicating(false) - setIsMenuOpen(false) + const handleTabClick = (tab: PanelTab) => { + if (!isPanelOpen) { + setIsPanelOpen(true) } - }, [activeWorkflowId, userPermissions.canEdit, isDuplicating, workflows, router, workspaceId]) - - /** - * Toggles the locked state of all blocks in the workflow - */ - const handleToggleWorkflowLock = useCallback(() => { - const blocks = useWorkflowStore.getState().blocks - const allLocked = Object.values(blocks).every((b) => b.locked) - const ids = getWorkflowLockToggleIds(blocks, !allLocked) - if (ids.length > 0) collaborativeBatchToggleLocked(ids) - setIsMenuOpen(false) - }, [collaborativeBatchToggleLocked]) - - // Compute run button state - const canRun = userPermissions.canRead // Running only requires read permissions - const isLoadingPermissions = userPermissions.isLoading - const hasValidationErrors = false // TODO: Add validation logic if needed - const isWorkflowBlocked = isExecuting || hasValidationErrors - const isButtonDisabled = !isExecuting && (isWorkflowBlocked || (!canRun && !isLoadingPermissions)) + setActiveTab(tab) + } /** * Register global keyboard shortcuts using the central commands registry. @@ -595,6 +401,7 @@ export const Panel = memo(function Panel({ workspaceId: propWorkspaceId }: Panel { id: 'focus-toolbar-search', handler: () => { + if (!isPanelOpen) setIsPanelOpen(true) setActiveTab('toolbar') toolbarRef.current?.focusSearch() }, @@ -605,274 +412,352 @@ export const Panel = memo(function Panel({ workspaceId: propWorkspaceId }: Panel ]) ) - return ( - <> - - - {/* Delete Confirmation Modal */} - - - Delete Workflow - -

- Are you sure you want to delete{' '} - - {currentWorkflow?.name ?? 'this workflow'} - - ?{' '} - - All associated blocks, executions, and configuration will be removed. - {' '} - - You can restore it from Recently Deleted in Settings. - -

-
- - - - -
-
- - {/* Floating Variables Modal */} ) 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 9272103bab7..a8b3b795eb8 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 @@ -61,7 +61,6 @@ import { sendMothershipMessage } from '@/stores/notifications/utils' import type { ConsoleEntry } from '@/stores/terminal' import { safeConsoleStringify, - useConsoleEntry, useTerminalConsoleStore, useTerminalStore, useWorkflowConsoleEntries, @@ -132,25 +131,36 @@ const BlockRow = memo(function BlockRow({
{entry.blockName}
- + {entry.startedAt && ( + + {new Date(entry.startedAt).toLocaleTimeString([], { + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + })} + )} - > - - + + + +
) }) @@ -553,15 +563,15 @@ function TerminalLogListRow({ if (row.rowType === 'separator') { return ( -
-
+
+
) } return ( -
-
+
+
(null) const prevWorkflowEntriesLengthRef = useRef(0) const hasInitializedEntriesRef = useRef(false) @@ -690,8 +705,10 @@ export const Terminal = memo(function Terminal() { const clearWorkflowConsole = useTerminalConsoleStore((state) => state.clearWorkflowConsole) const exportConsoleCSV = useTerminalConsoleStore((state) => state.exportConsoleCSV) - const [selectedEntryId, setSelectedEntryId] = useState(null) - const selectedEntry = useConsoleEntry(selectedEntryId) + const [selectedEntry, setSelectedEntry] = useState(null) + const selectedEntryId = selectedEntry?.id ?? null + const [panelOutputHeight, setPanelOutputHeight] = useState(0) + const splitContainerRef = useRef(null) const [expandedNodes, setExpandedNodes] = useState>(() => new Set()) const [isToggling, setIsToggling] = useState(false) const [showCopySuccess, setShowCopySuccess] = useState(false) @@ -904,13 +921,19 @@ export const Terminal = memo(function Terminal() { const handleSelectEntry = useCallback( (entry: ConsoleEntry) => { focusTerminal() - setSelectedEntryId((prev) => { - // Disable auto-select on any manual selection/deselection - setAutoSelectEnabled(false) - return prev === entry.id ? null : entry.id - }) + setAutoSelectEnabled(false) + if (isPanelMode) { + setSelectedEntry(entry) + if (panelOutputHeight === 0) { + const container = splitContainerRef.current + const h = container ? Math.round(container.clientHeight * 0.4) : 200 + setPanelOutputHeight(h) + } + } else { + setSelectedEntry((prev) => (prev?.id === entry.id ? null : entry)) + } }, - [focusTerminal] + [focusTerminal, isPanelMode, panelOutputHeight] ) /** @@ -963,7 +986,7 @@ export const Terminal = memo(function Terminal() { const clearCurrentWorkflowConsole = useCallback(() => { if (activeWorkflowId) { clearWorkflowConsole(activeWorkflowId) - setSelectedEntryId(null) + setSelectedEntry(null) setExpandedNodes(new Set()) } }, [activeWorkflowId, clearWorkflowConsole]) @@ -1094,7 +1117,7 @@ export const Terminal = memo(function Terminal() { useEffect(() => { if (executionGroups.length === 0 || navigableEntries.length === 0) { setAutoSelectEnabled(true) - setSelectedEntryId(null) + setSelectedEntry(null) return } @@ -1114,7 +1137,7 @@ export const Terminal = memo(function Terminal() { if (!lastNavEntry) return if (selectedEntryId === lastNavEntry.entry.id) return - setSelectedEntryId(lastNavEntry.entry.id) + setSelectedEntry(lastNavEntry.entry) focusTerminal() if (lastNavEntry.parentNodeIds.length > 0) { @@ -1128,6 +1151,23 @@ export const Terminal = memo(function Terminal() { } }, [executionGroups, navigableEntries, autoSelectEnabled, selectedEntryId, focusTerminal]) + useEffect(() => { + if (!selectedEntry) return + const updatedEntry = filteredEntries.find((e) => e.id === selectedEntry.id) + if (updatedEntry && updatedEntry !== selectedEntry) { + const hasChanged = + updatedEntry.output !== selectedEntry.output || + updatedEntry.isRunning !== selectedEntry.isRunning || + updatedEntry.isCanceled !== selectedEntry.isCanceled || + updatedEntry.durationMs !== selectedEntry.durationMs || + updatedEntry.error !== selectedEntry.error || + updatedEntry.success !== selectedEntry.success + if (hasChanged) { + setSelectedEntry(updatedEntry) + } + } + }, [filteredEntries, selectedEntry]) + /** * Clear filters when there are no logs */ @@ -1143,7 +1183,7 @@ export const Terminal = memo(function Terminal() { const navigateToEntry = useCallback( (navEntry: NavigableBlockEntry) => { setAutoSelectEnabled(false) - setSelectedEntryId(navEntry.entry.id) + setSelectedEntry(navEntry.entry) // Auto-expand parent nodes (subflows, iterations) if (navEntry.parentNodeIds.length > 0) { @@ -1183,7 +1223,7 @@ export const Terminal = memo(function Terminal() { if (e.key === 'Escape') { if (currentEntry) { e.preventDefault() - setSelectedEntryId(null) + setSelectedEntry(null) setAutoSelectEnabled(true) } return @@ -1250,20 +1290,26 @@ export const Terminal = memo(function Terminal() { const handleResize = () => { if (!selectedEntry) return - const sidebarWidth = Number.parseInt( - getComputedStyle(document.documentElement).getPropertyValue('--sidebar-width') || '0' - ) - const panelWidth = Number.parseInt( - getComputedStyle(document.documentElement).getPropertyValue('--panel-width') || '0' - ) + let terminalWidth: number + + if (isPanelMode && terminalRef.current) { + terminalWidth = terminalRef.current.clientWidth + } else { + const sidebarWidth = Number.parseInt( + getComputedStyle(document.documentElement).getPropertyValue('--sidebar-width') || '0' + ) + const panelWidth = Number.parseInt( + getComputedStyle(document.documentElement).getPropertyValue('--panel-width') || '0' + ) + terminalWidth = window.innerWidth - sidebarWidth - panelWidth + } - const terminalWidth = window.innerWidth - sidebarWidth - panelWidth const maxWidth = terminalWidth - TERMINAL_CONFIG.BLOCK_COLUMN_WIDTH_PX // Close output panel if there's not enough space for minimum width if (maxWidth < MIN_OUTPUT_PANEL_WIDTH_PX) { setAutoSelectEnabled(false) - setSelectedEntryId(null) + setSelectedEntry(null) return } @@ -1296,8 +1342,11 @@ export const Terminal = memo(function Terminal() {