Skip to content

Commit 09e73d8

Browse files
committed
Edit, plan, debug subagents
1 parent e959b88 commit 09e73d8

File tree

4 files changed

+100
-48
lines changed

4 files changed

+100
-48
lines changed

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

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,22 @@ const SUBAGENT_MAX_HEIGHT = 125
298298
*/
299299
const SUBAGENT_SCROLL_INTERVAL = 100
300300

301+
/**
302+
* Get display labels for subagent tools
303+
*/
304+
function getSubagentLabels(toolName: string, isStreaming: boolean): string {
305+
switch (toolName) {
306+
case 'debug':
307+
return isStreaming ? 'Debugging' : 'Debugged'
308+
case 'apply_edit':
309+
return isStreaming ? 'Applying edit' : 'Applied edit'
310+
case 'plan':
311+
return isStreaming ? 'Planning' : 'Planned'
312+
default:
313+
return isStreaming ? 'Processing' : 'Processed'
314+
}
315+
}
316+
301317
/**
302318
* SubAgentContent renders the streamed content and tool calls from a subagent
303319
* with thinking-style styling (same as ThinkingBlock).
@@ -306,9 +322,11 @@ const SUBAGENT_SCROLL_INTERVAL = 100
306322
function SubAgentContent({
307323
blocks,
308324
isStreaming = false,
325+
toolName = 'debug',
309326
}: {
310327
blocks?: SubAgentContentBlock[]
311328
isStreaming?: boolean
329+
toolName?: string
312330
}) {
313331
const [isExpanded, setIsExpanded] = useState(false)
314332
const userCollapsedRef = useRef<boolean>(false)
@@ -347,7 +365,7 @@ function SubAgentContent({
347365
if (!blocks || blocks.length === 0) return null
348366

349367
const hasContent = blocks.length > 0
350-
const label = isStreaming ? 'Debugging' : 'Debugged'
368+
const label = getSubagentLabels(toolName, isStreaming)
351369

352370
return (
353371
<div className='mt-1 mb-0'>
@@ -768,13 +786,15 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }:
768786
// Skip rendering some internal tools
769787
if (toolCall.name === 'checkoff_todo' || toolCall.name === 'mark_todo_in_progress') return null
770788

771-
// Special rendering for debug tool with subagent content - only show the collapsible SubAgentContent
772-
if (toolCall.name === 'debug' && toolCall.subAgentBlocks && toolCall.subAgentBlocks.length > 0) {
789+
// Special rendering for subagent tools (debug, apply_edit, plan) - only show the collapsible SubAgentContent
790+
const isSubagentTool = toolCall.name === 'debug' || toolCall.name === 'apply_edit' || toolCall.name === 'plan'
791+
if (isSubagentTool && toolCall.subAgentBlocks && toolCall.subAgentBlocks.length > 0) {
773792
return (
774793
<div className='w-full'>
775794
<SubAgentContent
776795
blocks={toolCall.subAgentBlocks}
777796
isStreaming={toolCall.subAgentStreaming}
797+
toolName={toolCall.name}
778798
/>
779799
</div>
780800
)
@@ -1209,6 +1229,7 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }:
12091229
<SubAgentContent
12101230
blocks={toolCall.subAgentBlocks}
12111231
isStreaming={toolCall.subAgentStreaming}
1232+
toolName={toolCall.name}
12121233
/>
12131234
)}
12141235
</div>
@@ -1271,6 +1292,7 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }:
12711292
<SubAgentContent
12721293
blocks={toolCall.subAgentBlocks}
12731294
isStreaming={toolCall.subAgentStreaming}
1295+
toolName={toolCall.name}
12741296
/>
12751297
)}
12761298
</div>
@@ -1380,6 +1402,7 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }:
13801402
<SubAgentContent
13811403
blocks={toolCall.subAgentBlocks}
13821404
isStreaming={toolCall.subAgentStreaming}
1405+
toolName={toolCall.name}
13831406
/>
13841407
)}
13851408
</div>
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { Loader2, Pencil, XCircle } from 'lucide-react'
2+
import {
3+
BaseClientTool,
4+
type BaseClientToolMetadata,
5+
ClientToolCallState,
6+
} from '@/lib/copilot/tools/client/base-tool'
7+
8+
interface ApplyEditArgs {
9+
instruction: string
10+
}
11+
12+
/**
13+
* Apply Edit tool that spawns a subagent to apply code/workflow edits.
14+
* This tool auto-executes and the actual work is done by the apply_edit subagent.
15+
* The subagent's output is streamed as nested content under this tool call.
16+
*/
17+
export class ApplyEditClientTool extends BaseClientTool {
18+
static readonly id = 'apply_edit'
19+
20+
constructor(toolCallId: string) {
21+
super(toolCallId, ApplyEditClientTool.id, ApplyEditClientTool.metadata)
22+
}
23+
24+
static readonly metadata: BaseClientToolMetadata = {
25+
displayNames: {
26+
[ClientToolCallState.generating]: { text: 'Preparing edit', icon: Loader2 },
27+
[ClientToolCallState.pending]: { text: 'Applying edit', icon: Loader2 },
28+
[ClientToolCallState.executing]: { text: 'Applying edit', icon: Loader2 },
29+
[ClientToolCallState.success]: { text: 'Edit applied', icon: Pencil },
30+
[ClientToolCallState.error]: { text: 'Failed to apply edit', icon: XCircle },
31+
[ClientToolCallState.rejected]: { text: 'Edit skipped', icon: XCircle },
32+
[ClientToolCallState.aborted]: { text: 'Edit aborted', icon: XCircle },
33+
},
34+
}
35+
36+
/**
37+
* Execute the apply_edit tool.
38+
* This just marks the tool as executing - the actual edit work is done server-side
39+
* by the apply_edit subagent, and its output is streamed as subagent events.
40+
*/
41+
async execute(_args?: ApplyEditArgs): Promise<void> {
42+
// Immediately transition to executing state - no user confirmation needed
43+
this.setState(ClientToolCallState.executing)
44+
// The tool result will come from the server via tool_result event
45+
// when the apply_edit subagent completes its work
46+
}
47+
}
48+
Lines changed: 23 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
1-
import { createLogger } from '@sim/logger'
2-
import { ListTodo, Loader2, X, XCircle } from 'lucide-react'
1+
import { ListTodo, Loader2, XCircle } from 'lucide-react'
32
import {
43
BaseClientTool,
54
type BaseClientToolMetadata,
65
ClientToolCallState,
76
} from '@/lib/copilot/tools/client/base-tool'
87

98
interface PlanArgs {
10-
objective?: string
11-
todoList?: Array<{ id?: string; content: string } | string>
9+
request: string
1210
}
1311

12+
/**
13+
* Plan tool that spawns a subagent to plan an approach.
14+
* This tool auto-executes and the actual work is done by the plan subagent.
15+
* The subagent's output is streamed as nested content under this tool call.
16+
*/
1417
export class PlanClientTool extends BaseClientTool {
1518
static readonly id = 'plan'
1619

@@ -20,50 +23,25 @@ export class PlanClientTool extends BaseClientTool {
2023

2124
static readonly metadata: BaseClientToolMetadata = {
2225
displayNames: {
23-
[ClientToolCallState.generating]: { text: 'Planning', icon: Loader2 },
26+
[ClientToolCallState.generating]: { text: 'Preparing plan', icon: Loader2 },
2427
[ClientToolCallState.pending]: { text: 'Planning', icon: Loader2 },
25-
[ClientToolCallState.executing]: { text: 'Planning an approach', icon: Loader2 },
26-
[ClientToolCallState.success]: { text: 'Finished planning', icon: ListTodo },
27-
[ClientToolCallState.error]: { text: 'Failed to plan', icon: X },
28-
[ClientToolCallState.aborted]: { text: 'Aborted planning', icon: XCircle },
29-
[ClientToolCallState.rejected]: { text: 'Skipped planning approach', icon: XCircle },
28+
[ClientToolCallState.executing]: { text: 'Planning', icon: Loader2 },
29+
[ClientToolCallState.success]: { text: 'Planned', icon: ListTodo },
30+
[ClientToolCallState.error]: { text: 'Failed to plan', icon: XCircle },
31+
[ClientToolCallState.rejected]: { text: 'Plan skipped', icon: XCircle },
32+
[ClientToolCallState.aborted]: { text: 'Plan aborted', icon: XCircle },
3033
},
3134
}
3235

33-
async execute(args?: PlanArgs): Promise<void> {
34-
const logger = createLogger('PlanClientTool')
35-
try {
36-
this.setState(ClientToolCallState.executing)
37-
38-
// Update store todos from args if present (client-side only)
39-
try {
40-
const todoList = args?.todoList
41-
if (Array.isArray(todoList)) {
42-
const todos = todoList.map((item: any, index: number) => ({
43-
id: (item && (item.id || item.todoId)) || `todo-${index}`,
44-
content: typeof item === 'string' ? item : item.content,
45-
completed: false,
46-
executing: false,
47-
}))
48-
const { useCopilotStore } = await import('@/stores/panel/copilot/store')
49-
const store = useCopilotStore.getState()
50-
if (store.setPlanTodos) {
51-
store.setPlanTodos(todos)
52-
useCopilotStore.setState({ showPlanTodos: true })
53-
}
54-
}
55-
} catch (e) {
56-
logger.warn('Failed to update plan todos in store', { message: (e as any)?.message })
57-
}
58-
59-
this.setState(ClientToolCallState.success)
60-
// Echo args back so store/tooling can parse todoList if needed
61-
await this.markToolComplete(200, 'Plan ready', args || {})
62-
this.setState(ClientToolCallState.success)
63-
} catch (e: any) {
64-
logger.error('execute failed', { message: e?.message })
65-
this.setState(ClientToolCallState.error)
66-
await this.markToolComplete(500, e?.message || 'Failed to plan')
67-
}
36+
/**
37+
* Execute the plan tool.
38+
* This just marks the tool as executing - the actual planning work is done server-side
39+
* by the plan subagent, and its output is streamed as subagent events.
40+
*/
41+
async execute(_args?: PlanArgs): Promise<void> {
42+
// Immediately transition to executing state - no user confirmation needed
43+
this.setState(ClientToolCallState.executing)
44+
// The tool result will come from the server via tool_result event
45+
// when the plan subagent completes its work
6846
}
6947
}

apps/sim/stores/panel/copilot/store.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import { PlanClientTool } from '@/lib/copilot/tools/client/other/plan'
3333
import { RememberDebugClientTool } from '@/lib/copilot/tools/client/other/remember-debug'
3434
import { SearchDocumentationClientTool } from '@/lib/copilot/tools/client/other/search-documentation'
3535
import { SearchErrorsClientTool } from '@/lib/copilot/tools/client/other/search-errors'
36+
import { ApplyEditClientTool } from '@/lib/copilot/tools/client/other/apply-edit'
3637
import { DebugClientTool } from '@/lib/copilot/tools/client/other/debug'
3738
import { SearchOnlineClientTool } from '@/lib/copilot/tools/client/other/search-online'
3839
import { SearchPatternsClientTool } from '@/lib/copilot/tools/client/other/search-patterns'
@@ -79,6 +80,7 @@ try {
7980

8081
// Known class-based client tools: map tool name -> instantiator
8182
const CLIENT_TOOL_INSTANTIATORS: Record<string, (id: string) => any> = {
83+
apply_edit: (id) => new ApplyEditClientTool(id),
8284
debug: (id) => new DebugClientTool(id),
8385
run_workflow: (id) => new RunWorkflowClientTool(id),
8486
get_workflow_console: (id) => new GetWorkflowConsoleClientTool(id),
@@ -122,6 +124,7 @@ const CLIENT_TOOL_INSTANTIATORS: Record<string, (id: string) => any> = {
122124

123125
// Read-only static metadata for class-based tools (no instances)
124126
export const CLASS_TOOL_METADATA: Record<string, BaseClientToolMetadata | undefined> = {
127+
apply_edit: (ApplyEditClientTool as any)?.metadata,
125128
debug: (DebugClientTool as any)?.metadata,
126129
run_workflow: (RunWorkflowClientTool as any)?.metadata,
127130
get_workflow_console: (GetWorkflowConsoleClientTool as any)?.metadata,

0 commit comments

Comments
 (0)