Skip to content

Commit 1f6f58c

Browse files
committed
improvement(copilot): diff controls
1 parent 41d767e commit 1f6f58c

File tree

13 files changed

+120
-99
lines changed

13 files changed

+120
-99
lines changed

apps/sim/app/workspace/[workspaceId]/utils/commands-utils.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type { GlobalCommand } from '@/app/workspace/[workspaceId]/providers/glob
77
* ad-hoc ids or shortcuts to ensure a single source of truth.
88
*/
99
export type CommandId =
10+
| 'accept-diff-changes'
1011
| 'add-agent'
1112
| 'goto-templates'
1213
| 'goto-logs'
@@ -43,6 +44,11 @@ export interface CommandDefinition {
4344
* All global commands must be declared here to be usable.
4445
*/
4546
export const COMMAND_DEFINITIONS: Record<CommandId, CommandDefinition> = {
47+
'accept-diff-changes': {
48+
id: 'accept-diff-changes',
49+
shortcut: 'Mod+Shift+Enter',
50+
allowInEditable: true,
51+
},
4652
'add-agent': {
4753
id: 'add-agent',
4854
shortcut: 'Mod+Shift+A',

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/diff-controls/diff-controls.tsx

Lines changed: 75 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import { memo, useCallback } from 'react'
1+
import { memo, useCallback, useMemo } from 'react'
22
import { createLogger } from '@sim/logger'
33
import clsx from 'clsx'
4-
import { Eye, EyeOff } from 'lucide-react'
5-
import { Button } from '@/components/emcn'
4+
import { useRegisterGlobalCommands } from '@/app/workspace/[workspaceId]/providers/global-commands-provider'
5+
import { createCommand } from '@/app/workspace/[workspaceId]/utils/commands-utils'
66
import { usePreventZoom } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks'
77
import { useCopilotStore } from '@/stores/panel/copilot/store'
8+
import { usePanelStore } from '@/stores/panel/store'
89
import { useTerminalStore } from '@/stores/terminal'
910
import { useWorkflowDiffStore } from '@/stores/workflow-diff'
1011
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
@@ -15,28 +16,20 @@ const logger = createLogger('DiffControls')
1516

1617
export const DiffControls = memo(function DiffControls() {
1718
const isTerminalResizing = useTerminalStore((state) => state.isResizing)
18-
const {
19-
isShowingDiff,
20-
isDiffReady,
21-
hasActiveDiff,
22-
toggleDiffView,
23-
acceptChanges,
24-
rejectChanges,
25-
baselineWorkflow,
26-
} = useWorkflowDiffStore(
27-
useCallback(
28-
(state) => ({
29-
isShowingDiff: state.isShowingDiff,
30-
isDiffReady: state.isDiffReady,
31-
hasActiveDiff: state.hasActiveDiff,
32-
toggleDiffView: state.toggleDiffView,
33-
acceptChanges: state.acceptChanges,
34-
rejectChanges: state.rejectChanges,
35-
baselineWorkflow: state.baselineWorkflow,
36-
}),
37-
[]
19+
const isPanelResizing = usePanelStore((state) => state.isResizing)
20+
const { isDiffReady, hasActiveDiff, acceptChanges, rejectChanges, baselineWorkflow } =
21+
useWorkflowDiffStore(
22+
useCallback(
23+
(state) => ({
24+
isDiffReady: state.isDiffReady,
25+
hasActiveDiff: state.hasActiveDiff,
26+
acceptChanges: state.acceptChanges,
27+
rejectChanges: state.rejectChanges,
28+
baselineWorkflow: state.baselineWorkflow,
29+
}),
30+
[]
31+
)
3832
)
39-
)
4033

4134
const { updatePreviewToolCallState, currentChat, messages } = useCopilotStore(
4235
useCallback(
@@ -53,11 +46,6 @@ export const DiffControls = memo(function DiffControls() {
5346
useCallback((state) => ({ activeWorkflowId: state.activeWorkflowId }), [])
5447
)
5548

56-
const handleToggleDiff = useCallback(() => {
57-
logger.info('Toggling diff view', { currentState: isShowingDiff })
58-
toggleDiffView()
59-
}, [isShowingDiff, toggleDiffView])
60-
6149
const createCheckpoint = useCallback(async () => {
6250
if (!activeWorkflowId || !currentChat?.id) {
6351
logger.warn('Cannot create checkpoint: missing workflowId or chatId', {
@@ -286,54 +274,82 @@ export const DiffControls = memo(function DiffControls() {
286274

287275
const preventZoomRef = usePreventZoom()
288276

277+
// Register global command to accept changes (Cmd/Ctrl + Shift + Enter)
278+
const acceptCommand = useMemo(
279+
() =>
280+
createCommand({
281+
id: 'accept-diff-changes',
282+
handler: () => {
283+
if (hasActiveDiff && isDiffReady) {
284+
handleAccept()
285+
}
286+
},
287+
}),
288+
[hasActiveDiff, isDiffReady, handleAccept]
289+
)
290+
useRegisterGlobalCommands([acceptCommand])
291+
289292
// Don't show anything if no diff is available or diff is not ready
290293
if (!hasActiveDiff || !isDiffReady) {
291294
return null
292295
}
293296

297+
const isResizing = isTerminalResizing || isPanelResizing
298+
294299
return (
295300
<div
296301
ref={preventZoomRef}
297302
className={clsx(
298-
'-translate-x-1/2 fixed left-1/2 z-30',
299-
!isTerminalResizing && 'transition-[bottom] duration-100 ease-out'
303+
'fixed z-30',
304+
!isResizing && 'transition-[bottom,right] duration-100 ease-out'
300305
)}
301-
style={{ bottom: 'calc(var(--terminal-height) + 40px)' }}
306+
style={{
307+
bottom: 'calc(var(--terminal-height) + 8px)',
308+
right: 'calc(var(--panel-width) + 8px)',
309+
}}
302310
>
303-
<div className='flex items-center gap-[6px] rounded-[10px] p-[6px]'>
304-
{/* Toggle (left, icon-only) */}
305-
<Button
306-
variant='active'
307-
onClick={handleToggleDiff}
308-
className='h-[30px] w-[30px] rounded-[8px] p-0'
309-
title={isShowingDiff ? 'View original' : 'Preview changes'}
310-
>
311-
{isShowingDiff ? (
312-
<Eye className='h-[14px] w-[14px]' />
313-
) : (
314-
<EyeOff className='h-[14px] w-[14px]' />
315-
)}
316-
</Button>
317-
318-
{/* Reject */}
319-
<Button
320-
variant='active'
311+
<div
312+
className='group relative flex h-[30px] overflow-hidden rounded-[4px]'
313+
style={{ isolation: 'isolate' }}
314+
>
315+
{/* Reject side */}
316+
<button
321317
onClick={handleReject}
322-
className='h-[30px] rounded-[8px] px-3'
323318
title='Reject changes'
319+
className='relative flex h-full items-center border border-[var(--border)] bg-[var(--surface-4)] pr-[20px] pl-[12px] font-medium text-[13px] text-[var(--text-secondary)] transition-colors hover:border-[var(--border-1)] hover:bg-[var(--surface-6)] hover:text-[var(--text-primary)] dark:hover:bg-[var(--surface-5)]'
320+
style={{
321+
clipPath: 'polygon(0 0, calc(100% + 10px) 0, 100% 100%, 0 100%)',
322+
borderRadius: '4px 0 0 4px',
323+
}}
324324
>
325325
Reject
326-
</Button>
327-
328-
{/* Accept */}
329-
<Button
330-
variant='tertiary'
326+
</button>
327+
{/* Slanted divider - split gray/green */}
328+
<div
329+
className='pointer-events-none absolute top-0 bottom-0 z-10'
330+
style={{
331+
left: '66px',
332+
width: '2px',
333+
transform: 'skewX(-18.4deg)',
334+
background:
335+
'linear-gradient(to right, var(--border) 50%, color-mix(in srgb, var(--brand-tertiary-2) 70%, black) 50%)',
336+
}}
337+
/>
338+
{/* Accept side */}
339+
<button
331340
onClick={handleAccept}
332-
className='h-[30px] rounded-[8px] px-3'
333-
title='Accept changes'
341+
title='Accept changes (⇧⌘⏎)'
342+
className='-ml-[10px] relative flex h-full items-center border border-[rgba(0,0,0,0.15)] bg-[var(--brand-tertiary-2)] pr-[12px] pl-[20px] font-medium text-[13px] text-[var(--text-inverse)] transition-[background-color,border-color,fill,stroke] hover:brightness-110 dark:border-[rgba(255,255,255,0.1)]'
343+
style={{
344+
clipPath: 'polygon(10px 0, 100% 0, 100% 100%, 0 100%)',
345+
borderRadius: '0 4px 4px 0',
346+
}}
334347
>
335348
Accept
336-
</Button>
349+
<kbd className='ml-2 rounded border border-white/20 bg-white/10 px-1.5 py-0.5 font-medium font-sans text-[10px]'>
350+
⇧⌘<span className='translate-y-[-1px]'></span>
351+
</kbd>
352+
</button>
337353
</div>
338354
</div>
339355
)

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/notifications/notifications.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
openCopilotWithMessage,
1212
useNotificationStore,
1313
} from '@/stores/notifications'
14+
import { useSidebarStore } from '@/stores/sidebar/store'
1415
import { useTerminalStore } from '@/stores/terminal'
1516
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
1617

@@ -19,7 +20,7 @@ const MAX_VISIBLE_NOTIFICATIONS = 4
1920

2021
/**
2122
* Notifications display component
22-
* Positioned in the bottom-right workspace area, aligned with terminal and panel spacing
23+
* Positioned in the bottom-left workspace area, reactive to sidebar width and terminal height
2324
* Shows both global notifications and workflow-specific notifications
2425
*/
2526
export const Notifications = memo(function Notifications() {
@@ -36,6 +37,7 @@ export const Notifications = memo(function Notifications() {
3637
.slice(0, MAX_VISIBLE_NOTIFICATIONS)
3738
}, [allNotifications, activeWorkflowId])
3839
const isTerminalResizing = useTerminalStore((state) => state.isResizing)
40+
const isSidebarResizing = useSidebarStore((state) => state.isResizing)
3941

4042
/**
4143
* Executes a notification action and handles side effects.
@@ -103,12 +105,14 @@ export const Notifications = memo(function Notifications() {
103105
return null
104106
}
105107

108+
const isResizing = isTerminalResizing || isSidebarResizing
109+
106110
return (
107111
<div
108112
ref={preventZoomRef}
109113
className={clsx(
110-
'fixed right-[calc(var(--panel-width)+16px)] bottom-[calc(var(--terminal-height)+16px)] z-30 flex flex-col items-end',
111-
!isTerminalResizing && 'transition-[bottom] duration-100 ease-out'
114+
'fixed bottom-[calc(var(--terminal-height)+16px)] left-[calc(var(--sidebar-width)+16px)] z-30 flex flex-col items-start',
115+
!isResizing && 'transition-[bottom,left] duration-100 ease-out'
112116
)}
113117
>
114118
{[...visibleNotifications].reverse().map((notification, index, stacked) => {

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/queued-messages/queued-messages.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use client'
22

33
import { useCallback, useState } from 'react'
4-
import { ArrowUp, ChevronDown, ChevronRight, MoreHorizontal, Trash2 } from 'lucide-react'
4+
import { ArrowUp, ChevronDown, ChevronRight, Trash2 } from 'lucide-react'
55
import { useCopilotStore } from '@/stores/panel/copilot/store'
66

77
/**
@@ -48,7 +48,6 @@ export function QueuedMessages() {
4848
{messageQueue.length} Queued
4949
</span>
5050
</div>
51-
<MoreHorizontal className='h-3 w-3 text-[var(--text-tertiary)]' />
5251
</button>
5352

5453
{/* Message list */}

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/tool-call/tool-call.tsx

Lines changed: 5 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import { useEffect, useMemo, useRef, useState } from 'react'
44
import clsx from 'clsx'
5-
import { ChevronUp } from 'lucide-react'
5+
import { ChevronUp, LayoutList } from 'lucide-react'
66
import { Button, Code } from '@/components/emcn'
77
import { ClientToolCallState } from '@/lib/copilot/tools/client/base-tool'
88
import { getClientTool } from '@/lib/copilot/tools/client/manager'
@@ -201,28 +201,10 @@ function PlanSteps({
201201

202202
return (
203203
<div className='mt-1.5 overflow-hidden rounded-[6px] border border-[var(--border-1)] bg-[var(--surface-1)]'>
204-
<div className='flex items-center justify-between border-[var(--border-1)] border-b bg-[var(--surface-2)] p-[8px]'>
205-
<div className='flex min-w-0 flex-1 items-center gap-[6px]'>
206-
<svg
207-
className='h-3 w-3 flex-shrink-0 text-[var(--text-tertiary)]'
208-
viewBox='0 0 24 24'
209-
fill='none'
210-
stroke='currentColor'
211-
strokeWidth='2'
212-
strokeLinecap='round'
213-
strokeLinejoin='round'
214-
>
215-
{/* Three horizontal lines with circles at different positions */}
216-
<line x1='4' y1='6' x2='20' y2='6' />
217-
<circle cx='8' cy='6' r='2' fill='currentColor' />
218-
<line x1='4' y1='12' x2='20' y2='12' />
219-
<circle cx='16' cy='12' r='2' fill='currentColor' />
220-
<line x1='4' y1='18' x2='20' y2='18' />
221-
<circle cx='10' cy='18' r='2' fill='currentColor' />
222-
</svg>
223-
<span className='font-medium text-[14px] text-[var(--text-primary)]'>To-dos</span>
224-
</div>
225-
<span className='flex-shrink-0 font-medium text-[14px] text-[var(--text-tertiary)]'>
204+
<div className='flex items-center gap-[8px] border-[var(--border-1)] border-b bg-[var(--surface-2)] p-[8px]'>
205+
<LayoutList className='ml-[2px] h-3 w-3 flex-shrink-0 text-[var(--text-tertiary)]' />
206+
<span className='font-medium text-[12px] text-[var(--text-primary)]'>To-dos</span>
207+
<span className='flex-shrink-0 font-medium text-[12px] text-[var(--text-tertiary)]'>
226208
{sortedSteps.length}
227209
</span>
228210
</div>

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/hooks/use-panel-resize.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useCallback, useEffect, useState } from 'react'
1+
import { useCallback, useEffect } from 'react'
22
import { PANEL_WIDTH } from '@/stores/constants'
33
import { usePanelStore } from '@/stores/panel/store'
44

@@ -10,15 +10,14 @@ import { usePanelStore } from '@/stores/panel/store'
1010
* @returns Resize state and handlers
1111
*/
1212
export function usePanelResize() {
13-
const { setPanelWidth } = usePanelStore()
14-
const [isResizing, setIsResizing] = useState(false)
13+
const { setPanelWidth, isResizing, setIsResizing } = usePanelStore()
1514

1615
/**
1716
* Handles mouse down on resize handle
1817
*/
1918
const handleMouseDown = useCallback(() => {
2019
setIsResizing(true)
21-
}, [])
20+
}, [setIsResizing])
2221

2322
/**
2423
* Setup resize event listeners and body styles when resizing
@@ -52,7 +51,7 @@ export function usePanelResize() {
5251
document.body.style.cursor = ''
5352
document.body.style.userSelect = ''
5453
}
55-
}, [isResizing, setPanelWidth])
54+
}, [isResizing, setPanelWidth, setIsResizing])
5655

5756
return {
5857
isResizing,

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/subflow-node.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ export const SubflowNodeComponent = memo(({ data, id }: NodeProps<SubflowNodeDat
136136
const ringStyles = cn(
137137
hasRing && 'ring-[1.75px]',
138138
(isFocused || isPreviewSelected) && 'ring-[var(--brand-secondary)]',
139-
diffStatus === 'new' && 'ring-[#22C55F]',
139+
diffStatus === 'new' && 'ring-[var(--brand-tertiary-2)]',
140140
diffStatus === 'edited' && 'ring-[var(--warning)]'
141141
)
142142

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-edge/workflow-edge.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ const WorkflowEdgeComponent = ({
9393
} else if (isErrorEdge) {
9494
color = 'var(--text-error)'
9595
} else if (edgeDiffStatus === 'new') {
96-
color = 'var(--brand-tertiary)'
96+
color = 'var(--brand-tertiary-2)'
9797
} else if (edgeRunStatus === 'success') {
9898
// Use green for preview mode, default for canvas execution
9999
color = previewExecutionStatus ? 'var(--brand-tertiary-2)' : 'var(--border-success)'

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3304,8 +3304,6 @@ const WorkflowContent = React.memo(() => {
33043304
<LazyChat />
33053305
</Suspense>
33063306

3307-
<DiffControls />
3308-
33093307
{/* Context Menus */}
33103308
<BlockContextMenu
33113309
isOpen={isBlockMenuOpen}
@@ -3361,6 +3359,8 @@ const WorkflowContent = React.memo(() => {
33613359
<Panel />
33623360
</div>
33633361

3362+
<DiffControls />
3363+
33643364
<Terminal />
33653365

33663366
{oauthModal && (

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-sidebar-resize.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useCallback, useEffect, useState } from 'react'
1+
import { useCallback, useEffect } from 'react'
22
import { SIDEBAR_WIDTH } from '@/stores/constants'
33
import { useSidebarStore } from '@/stores/sidebar/store'
44

@@ -10,8 +10,7 @@ import { useSidebarStore } from '@/stores/sidebar/store'
1010
* @returns Resize state and handlers
1111
*/
1212
export function useSidebarResize() {
13-
const { setSidebarWidth } = useSidebarStore()
14-
const [isResizing, setIsResizing] = useState(false)
13+
const { setSidebarWidth, isResizing, setIsResizing } = useSidebarStore()
1514

1615
/**
1716
* Handles mouse down on resize handle

0 commit comments

Comments
 (0)