Skip to content

Commit 6f3df27

Browse files
authored
feat(copilot+canvas rendering): add context window tracking to copilot and selectively render canvas components (#1622)
* Copilot context window * Fix pill * Speed up canvas * Fix react hook bug * Context pill div
1 parent 3dd36a8 commit 6f3df27

File tree

12 files changed

+2017
-1907
lines changed

12 files changed

+2017
-1907
lines changed

apps/sim/app/api/copilot/chat/route.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,8 @@ export async function POST(req: NextRequest) {
463463
logger.debug(`[${tracker.requestId}] Sent initial chatId event to client`)
464464
}
465465

466+
// Note: context_usage events are forwarded from sim-agent (which has accurate token counts)
467+
466468
// Start title generation in parallel if needed
467469
if (actualChatId && !currentChat?.title && conversationHistory.length === 0) {
468470
generateChatTitle(message)
@@ -594,6 +596,7 @@ export async function POST(req: NextRequest) {
594596
lastSafeDoneResponseId = responseIdFromDone
595597
}
596598
}
599+
// Note: context_usage events are forwarded from sim-agent
597600
break
598601

599602
case 'error':

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

Lines changed: 46 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { memo, useCallback } from 'react'
12
import { Eye, EyeOff } from 'lucide-react'
23
import { Button } from '@/components/ui/button'
34
import { createLogger } from '@/lib/logs/console/logger'
@@ -9,31 +10,46 @@ import { useWorkflowStore } from '@/stores/workflows/workflow/store'
910

1011
const logger = createLogger('DiffControls')
1112

12-
export function DiffControls() {
13-
const {
14-
isShowingDiff,
15-
isDiffReady,
16-
diffWorkflow,
17-
toggleDiffView,
18-
acceptChanges,
19-
rejectChanges,
20-
diffMetadata,
21-
} = useWorkflowDiffStore()
22-
23-
const { updatePreviewToolCallState, clearPreviewYaml, currentChat, messages } = useCopilotStore()
24-
const { activeWorkflowId } = useWorkflowRegistry()
13+
export const DiffControls = memo(function DiffControls() {
14+
// Optimized: Single diff store subscription
15+
const { isShowingDiff, isDiffReady, diffWorkflow, toggleDiffView, acceptChanges, rejectChanges } =
16+
useWorkflowDiffStore(
17+
useCallback(
18+
(state) => ({
19+
isShowingDiff: state.isShowingDiff,
20+
isDiffReady: state.isDiffReady,
21+
diffWorkflow: state.diffWorkflow,
22+
toggleDiffView: state.toggleDiffView,
23+
acceptChanges: state.acceptChanges,
24+
rejectChanges: state.rejectChanges,
25+
}),
26+
[]
27+
)
28+
)
29+
30+
// Optimized: Single copilot store subscription for needed values
31+
const { updatePreviewToolCallState, clearPreviewYaml, currentChat, messages } = useCopilotStore(
32+
useCallback(
33+
(state) => ({
34+
updatePreviewToolCallState: state.updatePreviewToolCallState,
35+
clearPreviewYaml: state.clearPreviewYaml,
36+
currentChat: state.currentChat,
37+
messages: state.messages,
38+
}),
39+
[]
40+
)
41+
)
2542

26-
// Don't show anything if no diff is available or diff is not ready
27-
if (!diffWorkflow || !isDiffReady) {
28-
return null
29-
}
43+
const { activeWorkflowId } = useWorkflowRegistry(
44+
useCallback((state) => ({ activeWorkflowId: state.activeWorkflowId }), [])
45+
)
3046

31-
const handleToggleDiff = () => {
47+
const handleToggleDiff = useCallback(() => {
3248
logger.info('Toggling diff view', { currentState: isShowingDiff })
3349
toggleDiffView()
34-
}
50+
}, [isShowingDiff, toggleDiffView])
3551

36-
const createCheckpoint = async () => {
52+
const createCheckpoint = useCallback(async () => {
3753
if (!activeWorkflowId || !currentChat?.id) {
3854
logger.warn('Cannot create checkpoint: missing workflowId or chatId', {
3955
workflowId: activeWorkflowId,
@@ -184,9 +200,9 @@ export function DiffControls() {
184200
logger.error('Failed to create checkpoint:', error)
185201
return false
186202
}
187-
}
203+
}, [activeWorkflowId, currentChat, messages])
188204

189-
const handleAccept = async () => {
205+
const handleAccept = useCallback(async () => {
190206
logger.info('Accepting proposed changes with backup protection')
191207

192208
try {
@@ -239,9 +255,9 @@ export function DiffControls() {
239255
console.error('Workflow update failed:', errorMessage)
240256
alert(`Failed to save workflow changes: ${errorMessage}`)
241257
}
242-
}
258+
}, [createCheckpoint, clearPreviewYaml, updatePreviewToolCallState, acceptChanges])
243259

244-
const handleReject = () => {
260+
const handleReject = useCallback(() => {
245261
logger.info('Rejecting proposed changes (optimistic)')
246262

247263
// Clear preview YAML immediately
@@ -279,6 +295,11 @@ export function DiffControls() {
279295
rejectChanges().catch((error) => {
280296
logger.error('Failed to reject changes (background):', error)
281297
})
298+
}, [clearPreviewYaml, updatePreviewToolCallState, rejectChanges])
299+
300+
// Don't show anything if no diff is available or diff is not ready
301+
if (!diffWorkflow || !isDiffReady) {
302+
return null
282303
}
283304

284305
return (
@@ -319,4 +340,4 @@ export function DiffControls() {
319340
</div>
320341
</div>
321342
)
322-
}
343+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
'use client'
2+
3+
import { memo } from 'react'
4+
import { Plus } from 'lucide-react'
5+
import { cn } from '@/lib/utils'
6+
7+
interface ContextUsagePillProps {
8+
percentage: number
9+
className?: string
10+
onCreateNewChat?: () => void
11+
}
12+
13+
export const ContextUsagePill = memo(
14+
({ percentage, className, onCreateNewChat }: ContextUsagePillProps) => {
15+
// Don't render if invalid (but DO render if 0 or very small)
16+
if (percentage === null || percentage === undefined || Number.isNaN(percentage)) return null
17+
18+
const isHighUsage = percentage >= 75
19+
20+
// Determine color based on percentage (similar to Cursor IDE)
21+
const getColorClass = () => {
22+
if (percentage >= 90) return 'bg-red-500/10 text-red-600 dark:text-red-400'
23+
if (percentage >= 75) return 'bg-orange-500/10 text-orange-600 dark:text-orange-400'
24+
if (percentage >= 50) return 'bg-yellow-500/10 text-yellow-600 dark:text-yellow-400'
25+
return 'bg-gray-500/10 text-gray-600 dark:text-gray-400'
26+
}
27+
28+
// Format: show 1 decimal for <1%, 0 decimals for >=1%
29+
const formattedPercentage = percentage < 1 ? percentage.toFixed(1) : percentage.toFixed(0)
30+
31+
return (
32+
<div
33+
className={cn(
34+
'inline-flex items-center gap-1 rounded-full px-2 py-0.5 font-medium text-[11px] tabular-nums transition-colors',
35+
getColorClass(),
36+
isHighUsage && 'border border-red-500/50',
37+
className
38+
)}
39+
title={`Context used in this chat: ${percentage.toFixed(2)}%`}
40+
>
41+
<span>{formattedPercentage}%</span>
42+
{isHighUsage && onCreateNewChat && (
43+
<button
44+
onClick={(e) => {
45+
e.stopPropagation()
46+
onCreateNewChat()
47+
}}
48+
className='inline-flex items-center justify-center transition-opacity hover:opacity-70'
49+
title='Recommended: Start a new chat for better quality'
50+
type='button'
51+
>
52+
<Plus className='h-3 w-3' />
53+
</button>
54+
)}
55+
</div>
56+
)
57+
}
58+
)
59+
60+
ContextUsagePill.displayName = 'ContextUsagePill'

0 commit comments

Comments
 (0)