Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
useWorkspacePermissions,
type WorkspacePermissions,
} from '@/hooks/use-workspace-permissions'
import { useNotificationStore } from '@/stores/notifications'

const logger = createLogger('WorkspacePermissionsProvider')

Expand Down Expand Up @@ -60,16 +61,46 @@ export function WorkspacePermissionsProvider({ children }: WorkspacePermissionsP
// Manage offline mode state locally
const [isOfflineMode, setIsOfflineMode] = useState(false)

// Track whether we've already surfaced an offline notification to avoid duplicates
const [hasShownOfflineNotification, setHasShownOfflineNotification] = useState(false)

// Get operation error state from collaborative workflow
const { hasOperationError } = useCollaborativeWorkflow()

const addNotification = useNotificationStore((state) => state.addNotification)

// Set offline mode when there are operation errors
useEffect(() => {
if (hasOperationError) {
setIsOfflineMode(true)
}
}, [hasOperationError])

/**
* Surface a global notification when entering offline mode.
* Uses the shared notifications system instead of bespoke UI in individual components.
*/
useEffect(() => {
if (!isOfflineMode || hasShownOfflineNotification) {
return
}

try {
addNotification({
level: 'error',
message: 'Connection unavailable',
// Global notification (no workflowId) so it is visible regardless of the active workflow
action: {
type: 'refresh',
message: '',
},
})
setHasShownOfflineNotification(true)
} catch (error) {
logger.error('Failed to add offline notification', { error })
}
}, [addNotification, hasShownOfflineNotification, isOfflineMode])

// Fetch workspace permissions and loading state
const {
permissions: workspacePermissions,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -601,6 +601,7 @@ export function Chat() {
disabled={!activeWorkflowId}
placeholder='Select outputs'
align='end'
maxHeight={180}
/>
</div>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ interface OutputSelectProps {
placeholder?: string
valueMode?: 'id' | 'label'
align?: 'start' | 'end' | 'center'
maxHeight?: number
}

export function OutputSelect({
Expand All @@ -34,6 +35,7 @@ export function OutputSelect({
placeholder = 'Select outputs',
valueMode = 'id',
align = 'start',
maxHeight = 300,
}: OutputSelectProps) {
const [open, setOpen] = useState(false)
const [highlightedIndex, setHighlightedIndex] = useState(-1)
Expand Down Expand Up @@ -369,9 +371,9 @@ export function OutputSelect({
side='bottom'
align={align}
sideOffset={4}
maxHeight={300}
maxWidth={300}
minWidth={200}
maxHeight={maxHeight}
maxWidth={160}
minWidth={160}
onKeyDown={handleKeyDown}
tabIndex={0}
style={{ outline: 'none' }}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export { ControlBar } from './control-bar/control-bar'
export { Cursors } from './cursors/cursors'
export { DiffControls } from './diff-controls/diff-controls'
export { ErrorBoundary } from './error/index'
export { Notifications } from './notifications/notifications'
export { Panel } from './panel-new/panel-new'
export { SkeletonLoading } from './skeleton-loading/skeleton-loading'
export { SubflowNodeComponent } from './subflows/subflow-node'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { memo, useCallback } from 'react'
import { X } from 'lucide-react'
import { useParams } from 'next/navigation'
import { Button } from '@/components/emcn'
import { createLogger } from '@/lib/logs/console/logger'
import {
type NotificationAction,
openCopilotWithMessage,
useNotificationStore,
} from '@/stores/notifications'

const logger = createLogger('Notifications')
const MAX_VISIBLE_NOTIFICATIONS = 4

/**
* Notifications display component
* Positioned in the bottom-right workspace area, aligned with terminal and panel spacing
* Shows both global notifications and workflow-specific notifications
*/
export const Notifications = memo(function Notifications() {
const params = useParams()
const workflowId = params.workflowId as string

const notifications = useNotificationStore((state) =>
state.notifications.filter((n) => !n.workflowId || n.workflowId === workflowId)
)
const removeNotification = useNotificationStore((state) => state.removeNotification)
const visibleNotifications = notifications.slice(0, MAX_VISIBLE_NOTIFICATIONS)

/**
* Executes a notification action and handles side effects.
*
* @param notificationId - The ID of the notification whose action is executed.
* @param action - The action configuration to execute.
*/
const executeAction = useCallback(
(notificationId: string, action: NotificationAction) => {
try {
logger.info('Executing notification action', {
notificationId,
actionType: action.type,
messageLength: action.message.length,
})

switch (action.type) {
case 'copilot':
openCopilotWithMessage(action.message)
break
case 'refresh':
window.location.reload()
break
default:
logger.warn('Unknown action type', { notificationId, actionType: action.type })
}

// Dismiss the notification after the action is triggered
removeNotification(notificationId)
} catch (error) {
logger.error('Failed to execute notification action', {
notificationId,
actionType: action.type,
error,
})
}
},
[removeNotification]
)

if (visibleNotifications.length === 0) {
return null
}

return (
<div className='fixed right-[calc(var(--panel-width)+16px)] bottom-[calc(var(--terminal-height)+16px)] z-30 flex flex-col items-end'>
{[...visibleNotifications].reverse().map((notification, index, stacked) => {
const depth = stacked.length - index - 1
const xOffset = depth * 3

return (
<div
key={notification.id}
style={{ transform: `translateX(${xOffset}px)` }}
className={`relative w-[240px] rounded-[4px] border bg-[#232323] transition-transform duration-200 ${
index > 0 ? '-mt-[78px]' : ''
}`}
>
<div className='flex flex-col gap-[6px] px-[8px] pt-[6px] pb-[8px]'>
<div className='line-clamp-6 font-medium text-[12px] leading-[16px]'>
<Button
variant='ghost'
onClick={() => removeNotification(notification.id)}
aria-label='Dismiss notification'
className='!p-1.5 -m-1.5 float-right ml-[16px]'
>
<X className='h-3 w-3' />
</Button>
{notification.level === 'error' && (
<span className='mr-[6px] mb-[2.75px] inline-block h-[6px] w-[6px] rounded-[2px] bg-[var(--text-error)] align-middle' />
)}
{notification.message}
</div>
{notification.action && (
<Button
variant='active'
onClick={() => executeAction(notification.id, notification.action!)}
className='px-[8px] py-[4px] font-medium text-[12px]'
>
{notification.action.type === 'copilot'
? 'Fix in Copilot'
: notification.action.type === 'refresh'
? 'Refresh'
: 'Take action'}
</Button>
)}
</div>
</div>
)
})}
</div>
)
})
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { useDeleteWorkflow, useImportWorkflow } from '@/app/workspace/[workspace
import { useChatStore } from '@/stores/chat/store'
import { usePanelStore } from '@/stores/panel-new/store'
import type { PanelTab } from '@/stores/panel-new/types'
import { DEFAULT_TERMINAL_HEIGHT, MIN_TERMINAL_HEIGHT, useTerminalStore } from '@/stores/terminal'
import { useVariablesStore } from '@/stores/variables/store'
import { useWorkflowJsonStore } from '@/stores/workflows/json/store'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
Expand Down Expand Up @@ -130,6 +131,10 @@ export function Panel() {
openSubscriptionSettings()
return
}
const { openOnRun, terminalHeight, setTerminalHeight } = useTerminalStore.getState()
if (openOnRun && terminalHeight <= MIN_TERMINAL_HEIGHT) {
setTerminalHeight(DEFAULT_TERMINAL_HEIGHT)
}
await handleRunWorkflow()
}, [usageExceeded, handleRunWorkflow])

Expand Down
Loading