From 1f6f58cf7f745832e8aacaa3d7e923139d2c0f90 Mon Sep 17 00:00:00 2001 From: Emir Karabeg Date: Fri, 9 Jan 2026 20:17:49 -0800 Subject: [PATCH] improvement(copilot): diff controls --- .../[workspaceId]/utils/commands-utils.ts | 6 + .../diff-controls/diff-controls.tsx | 134 ++++++++++-------- .../notifications/notifications.tsx | 10 +- .../queued-messages/queued-messages.tsx | 3 +- .../components/tool-call/tool-call.tsx | 28 +--- .../panel/hooks/use-panel-resize.ts | 9 +- .../components/subflows/subflow-node.tsx | 2 +- .../workflow-edge/workflow-edge.tsx | 2 +- .../[workspaceId]/w/[workflowId]/workflow.tsx | 4 +- .../sidebar/hooks/use-sidebar-resize.ts | 5 +- apps/sim/stores/panel/store.ts | 4 + apps/sim/stores/panel/types.ts | 4 + apps/sim/stores/sidebar/store.ts | 8 ++ 13 files changed, 120 insertions(+), 99 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/utils/commands-utils.ts b/apps/sim/app/workspace/[workspaceId]/utils/commands-utils.ts index c9d74cced8..da68876e21 100644 --- a/apps/sim/app/workspace/[workspaceId]/utils/commands-utils.ts +++ b/apps/sim/app/workspace/[workspaceId]/utils/commands-utils.ts @@ -7,6 +7,7 @@ import type { GlobalCommand } from '@/app/workspace/[workspaceId]/providers/glob * ad-hoc ids or shortcuts to ensure a single source of truth. */ export type CommandId = + | 'accept-diff-changes' | 'add-agent' | 'goto-templates' | 'goto-logs' @@ -43,6 +44,11 @@ export interface CommandDefinition { * All global commands must be declared here to be usable. */ export const COMMAND_DEFINITIONS: Record = { + 'accept-diff-changes': { + id: 'accept-diff-changes', + shortcut: 'Mod+Shift+Enter', + allowInEditable: true, + }, 'add-agent': { id: 'add-agent', shortcut: 'Mod+Shift+A', diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/diff-controls/diff-controls.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/diff-controls/diff-controls.tsx index d8424fbfa9..cf72250333 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/diff-controls/diff-controls.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/diff-controls/diff-controls.tsx @@ -1,10 +1,11 @@ -import { memo, useCallback } from 'react' +import { memo, useCallback, useMemo } from 'react' import { createLogger } from '@sim/logger' import clsx from 'clsx' -import { Eye, EyeOff } from 'lucide-react' -import { Button } from '@/components/emcn' +import { useRegisterGlobalCommands } from '@/app/workspace/[workspaceId]/providers/global-commands-provider' +import { createCommand } from '@/app/workspace/[workspaceId]/utils/commands-utils' import { usePreventZoom } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks' import { useCopilotStore } from '@/stores/panel/copilot/store' +import { usePanelStore } from '@/stores/panel/store' import { useTerminalStore } from '@/stores/terminal' import { useWorkflowDiffStore } from '@/stores/workflow-diff' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' @@ -15,28 +16,20 @@ const logger = createLogger('DiffControls') export const DiffControls = memo(function DiffControls() { const isTerminalResizing = useTerminalStore((state) => state.isResizing) - const { - isShowingDiff, - isDiffReady, - hasActiveDiff, - toggleDiffView, - acceptChanges, - rejectChanges, - baselineWorkflow, - } = useWorkflowDiffStore( - useCallback( - (state) => ({ - isShowingDiff: state.isShowingDiff, - isDiffReady: state.isDiffReady, - hasActiveDiff: state.hasActiveDiff, - toggleDiffView: state.toggleDiffView, - acceptChanges: state.acceptChanges, - rejectChanges: state.rejectChanges, - baselineWorkflow: state.baselineWorkflow, - }), - [] + const isPanelResizing = usePanelStore((state) => state.isResizing) + const { isDiffReady, hasActiveDiff, acceptChanges, rejectChanges, baselineWorkflow } = + useWorkflowDiffStore( + useCallback( + (state) => ({ + isDiffReady: state.isDiffReady, + hasActiveDiff: state.hasActiveDiff, + acceptChanges: state.acceptChanges, + rejectChanges: state.rejectChanges, + baselineWorkflow: state.baselineWorkflow, + }), + [] + ) ) - ) const { updatePreviewToolCallState, currentChat, messages } = useCopilotStore( useCallback( @@ -53,11 +46,6 @@ export const DiffControls = memo(function DiffControls() { useCallback((state) => ({ activeWorkflowId: state.activeWorkflowId }), []) ) - const handleToggleDiff = useCallback(() => { - logger.info('Toggling diff view', { currentState: isShowingDiff }) - toggleDiffView() - }, [isShowingDiff, toggleDiffView]) - const createCheckpoint = useCallback(async () => { if (!activeWorkflowId || !currentChat?.id) { logger.warn('Cannot create checkpoint: missing workflowId or chatId', { @@ -286,54 +274,82 @@ export const DiffControls = memo(function DiffControls() { const preventZoomRef = usePreventZoom() + // Register global command to accept changes (Cmd/Ctrl + Shift + Enter) + const acceptCommand = useMemo( + () => + createCommand({ + id: 'accept-diff-changes', + handler: () => { + if (hasActiveDiff && isDiffReady) { + handleAccept() + } + }, + }), + [hasActiveDiff, isDiffReady, handleAccept] + ) + useRegisterGlobalCommands([acceptCommand]) + // Don't show anything if no diff is available or diff is not ready if (!hasActiveDiff || !isDiffReady) { return null } + const isResizing = isTerminalResizing || isPanelResizing + return (
-
- {/* Toggle (left, icon-only) */} - - - {/* Reject */} - - - {/* Accept */} - + + ⇧⌘ + +
) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/notifications/notifications.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/notifications/notifications.tsx index 24936e8e47..377dfea013 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/notifications/notifications.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/notifications/notifications.tsx @@ -11,6 +11,7 @@ import { openCopilotWithMessage, useNotificationStore, } from '@/stores/notifications' +import { useSidebarStore } from '@/stores/sidebar/store' import { useTerminalStore } from '@/stores/terminal' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' @@ -19,7 +20,7 @@ const MAX_VISIBLE_NOTIFICATIONS = 4 /** * Notifications display component - * Positioned in the bottom-right workspace area, aligned with terminal and panel spacing + * Positioned in the bottom-left workspace area, reactive to sidebar width and terminal height * Shows both global notifications and workflow-specific notifications */ export const Notifications = memo(function Notifications() { @@ -36,6 +37,7 @@ export const Notifications = memo(function Notifications() { .slice(0, MAX_VISIBLE_NOTIFICATIONS) }, [allNotifications, activeWorkflowId]) const isTerminalResizing = useTerminalStore((state) => state.isResizing) + const isSidebarResizing = useSidebarStore((state) => state.isResizing) /** * Executes a notification action and handles side effects. @@ -103,12 +105,14 @@ export const Notifications = memo(function Notifications() { return null } + const isResizing = isTerminalResizing || isSidebarResizing + return (
{[...visibleNotifications].reverse().map((notification, index, stacked) => { diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/queued-messages/queued-messages.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/queued-messages/queued-messages.tsx index 37a1d8e1c9..2ed3200345 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/queued-messages/queued-messages.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/queued-messages/queued-messages.tsx @@ -1,7 +1,7 @@ 'use client' import { useCallback, useState } from 'react' -import { ArrowUp, ChevronDown, ChevronRight, MoreHorizontal, Trash2 } from 'lucide-react' +import { ArrowUp, ChevronDown, ChevronRight, Trash2 } from 'lucide-react' import { useCopilotStore } from '@/stores/panel/copilot/store' /** @@ -48,7 +48,6 @@ export function QueuedMessages() { {messageQueue.length} Queued
- {/* Message list */} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/tool-call/tool-call.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/tool-call/tool-call.tsx index 9233ae45f9..5d404104ee 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/tool-call/tool-call.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/tool-call/tool-call.tsx @@ -2,7 +2,7 @@ import { useEffect, useMemo, useRef, useState } from 'react' import clsx from 'clsx' -import { ChevronUp } from 'lucide-react' +import { ChevronUp, LayoutList } from 'lucide-react' import { Button, Code } from '@/components/emcn' import { ClientToolCallState } from '@/lib/copilot/tools/client/base-tool' import { getClientTool } from '@/lib/copilot/tools/client/manager' @@ -201,28 +201,10 @@ function PlanSteps({ return (
-
-
- - {/* Three horizontal lines with circles at different positions */} - - - - - - - - To-dos -
- +
+ + To-dos + {sortedSteps.length}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/hooks/use-panel-resize.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/hooks/use-panel-resize.ts index 1d4f700287..3d72bc82b9 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/hooks/use-panel-resize.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/hooks/use-panel-resize.ts @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useState } from 'react' +import { useCallback, useEffect } from 'react' import { PANEL_WIDTH } from '@/stores/constants' import { usePanelStore } from '@/stores/panel/store' @@ -10,15 +10,14 @@ import { usePanelStore } from '@/stores/panel/store' * @returns Resize state and handlers */ export function usePanelResize() { - const { setPanelWidth } = usePanelStore() - const [isResizing, setIsResizing] = useState(false) + const { setPanelWidth, isResizing, setIsResizing } = usePanelStore() /** * Handles mouse down on resize handle */ const handleMouseDown = useCallback(() => { setIsResizing(true) - }, []) + }, [setIsResizing]) /** * Setup resize event listeners and body styles when resizing @@ -52,7 +51,7 @@ export function usePanelResize() { document.body.style.cursor = '' document.body.style.userSelect = '' } - }, [isResizing, setPanelWidth]) + }, [isResizing, setPanelWidth, setIsResizing]) return { isResizing, diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/subflow-node.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/subflow-node.tsx index fd1aa196ec..15ad8dce8f 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/subflow-node.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/subflow-node.tsx @@ -136,7 +136,7 @@ export const SubflowNodeComponent = memo(({ data, id }: NodeProps { - - {/* Context Menus */} {
+ + {oauthModal && ( diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-sidebar-resize.ts b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-sidebar-resize.ts index 0dbe6085b0..69b7877c06 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-sidebar-resize.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-sidebar-resize.ts @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useState } from 'react' +import { useCallback, useEffect } from 'react' import { SIDEBAR_WIDTH } from '@/stores/constants' import { useSidebarStore } from '@/stores/sidebar/store' @@ -10,8 +10,7 @@ import { useSidebarStore } from '@/stores/sidebar/store' * @returns Resize state and handlers */ export function useSidebarResize() { - const { setSidebarWidth } = useSidebarStore() - const [isResizing, setIsResizing] = useState(false) + const { setSidebarWidth, isResizing, setIsResizing } = useSidebarStore() /** * Handles mouse down on resize handle diff --git a/apps/sim/stores/panel/store.ts b/apps/sim/stores/panel/store.ts index dfa2b8fcd9..5e7d0c7401 100644 --- a/apps/sim/stores/panel/store.ts +++ b/apps/sim/stores/panel/store.ts @@ -29,6 +29,10 @@ export const usePanelStore = create()( document.documentElement.removeAttribute('data-panel-active-tab') } }, + isResizing: false, + setIsResizing: (isResizing) => { + set({ isResizing }) + }, _hasHydrated: false, setHasHydrated: (hasHydrated) => { set({ _hasHydrated: hasHydrated }) diff --git a/apps/sim/stores/panel/types.ts b/apps/sim/stores/panel/types.ts index f514301a29..dc35074750 100644 --- a/apps/sim/stores/panel/types.ts +++ b/apps/sim/stores/panel/types.ts @@ -11,6 +11,10 @@ export interface PanelState { setPanelWidth: (width: number) => void activeTab: PanelTab setActiveTab: (tab: PanelTab) => void + /** Whether the panel is currently being resized */ + isResizing: boolean + /** Updates the panel resize state */ + setIsResizing: (isResizing: boolean) => void _hasHydrated: boolean setHasHydrated: (hasHydrated: boolean) => void } diff --git a/apps/sim/stores/sidebar/store.ts b/apps/sim/stores/sidebar/store.ts index 42d13c4365..b05488bc31 100644 --- a/apps/sim/stores/sidebar/store.ts +++ b/apps/sim/stores/sidebar/store.ts @@ -9,10 +9,14 @@ interface SidebarState { workspaceDropdownOpen: boolean sidebarWidth: number isCollapsed: boolean + /** Whether the sidebar is currently being resized */ + isResizing: boolean _hasHydrated: boolean setWorkspaceDropdownOpen: (isOpen: boolean) => void setSidebarWidth: (width: number) => void setIsCollapsed: (isCollapsed: boolean) => void + /** Updates the sidebar resize state */ + setIsResizing: (isResizing: boolean) => void setHasHydrated: (hasHydrated: boolean) => void } @@ -22,6 +26,7 @@ export const useSidebarStore = create()( workspaceDropdownOpen: false, sidebarWidth: SIDEBAR_WIDTH.DEFAULT, isCollapsed: false, + isResizing: false, _hasHydrated: false, setWorkspaceDropdownOpen: (isOpen) => set({ workspaceDropdownOpen: isOpen }), setSidebarWidth: (width) => { @@ -44,6 +49,9 @@ export const useSidebarStore = create()( document.documentElement.style.setProperty('--sidebar-width', `${currentWidth}px`) } }, + setIsResizing: (isResizing) => { + set({ isResizing }) + }, setHasHydrated: (hasHydrated) => set({ _hasHydrated: hasHydrated }), }), {