@@ -26,9 +26,6 @@ import { CLASS_TOOL_METADATA } from '@/stores/panel/copilot/store'
2626import type { SubAgentContentBlock } from '@/stores/panel/copilot/types'
2727import { useWorkflowStore } from '@/stores/workflows/workflow/store'
2828
29- /**
30- * Parse special tags from content
31- */
3229/**
3330 * Plan step can be either a string or an object with title and plan
3431 */
@@ -47,6 +44,62 @@ interface ParsedTags {
4744 cleanContent : string
4845}
4946
47+ /**
48+ * Extract plan steps from plan_respond tool calls in subagent blocks.
49+ * Returns { steps, isComplete } where steps is in the format expected by PlanSteps component.
50+ */
51+ function extractPlanFromBlocks ( blocks : SubAgentContentBlock [ ] | undefined ) : {
52+ steps : Record < string , PlanStep > | undefined
53+ isComplete : boolean
54+ } {
55+ if ( ! blocks ) return { steps : undefined , isComplete : false }
56+
57+ // Find the plan_respond tool call
58+ const planRespondBlock = blocks . find (
59+ ( b ) => b . type === 'subagent_tool_call' && b . toolCall ?. name === 'plan_respond'
60+ )
61+
62+ if ( ! planRespondBlock ?. toolCall ) {
63+ return { steps : undefined , isComplete : false }
64+ }
65+
66+ // Tool call arguments can be in different places depending on the source
67+ // Also handle nested data.arguments structure from the schema
68+ const tc = planRespondBlock . toolCall as any
69+ const args =
70+ tc . params ||
71+ tc . parameters ||
72+ tc . input ||
73+ tc . arguments ||
74+ tc . data ?. arguments ||
75+ { }
76+ const stepsArray = args . steps
77+
78+ if ( ! Array . isArray ( stepsArray ) || stepsArray . length === 0 ) {
79+ return { steps : undefined , isComplete : false }
80+ }
81+
82+ // Convert array format to Record<string, PlanStep> format
83+ // From: [{ number: 1, title: "..." }, { number: 2, title: "..." }]
84+ // To: { "1": "...", "2": "..." }
85+ const steps : Record < string , PlanStep > = { }
86+ for ( const step of stepsArray ) {
87+ if ( step . number !== undefined && step . title ) {
88+ steps [ String ( step . number ) ] = step . title
89+ }
90+ }
91+
92+ // Check if the tool call is complete (not pending/executing)
93+ const isComplete =
94+ planRespondBlock . toolCall . state === ClientToolCallState . success ||
95+ planRespondBlock . toolCall . state === ClientToolCallState . error
96+
97+ return {
98+ steps : Object . keys ( steps ) . length > 0 ? steps : undefined ,
99+ isComplete,
100+ }
101+ }
102+
50103/**
51104 * Try to parse partial JSON for streaming options.
52105 * Attempts to extract complete key-value pairs from incomplete JSON.
@@ -1094,17 +1147,12 @@ function SubAgentContent({
10941147 } ) }
10951148 </ div >
10961149
1097- { /* Render PlanSteps for plan subagent when content contains <plan> tag */ }
1150+ { /* Render PlanSteps for plan subagent when plan_respond tool is present */ }
10981151 { toolName === 'plan' &&
10991152 ( ( ) => {
1100- // Combine all text content from blocks
1101- const allText = blocks
1102- . filter ( ( b ) => b . type === 'subagent_text' && b . content )
1103- . map ( ( b ) => b . content )
1104- . join ( '' )
1105- const parsed = parseSpecialTags ( allText )
1106- if ( parsed . plan && Object . keys ( parsed . plan ) . length > 0 ) {
1107- return < PlanSteps steps = { parsed . plan } streaming = { ! isThinkingDone } />
1153+ const { steps, isComplete } = extractPlanFromBlocks ( blocks )
1154+ if ( steps && Object . keys ( steps ) . length > 0 ) {
1155+ return < PlanSteps steps = { steps } streaming = { ! isComplete } />
11081156 }
11091157 return null
11101158 } ) ( ) }
@@ -1124,23 +1172,19 @@ function SubAgentThinkingContent({
11241172 isStreaming ?: boolean
11251173} ) {
11261174 // Combine all text content from blocks
1127- let allRawText = ''
11281175 let cleanText = ''
11291176 for ( const block of blocks ) {
11301177 if ( block . type === 'subagent_text' && block . content ) {
1131- allRawText += block . content
11321178 const parsed = parseSpecialTags ( block . content )
11331179 cleanText += parsed . cleanContent
11341180 }
11351181 }
11361182
1137- // Parse plan from all text
1138- const allParsed = parseSpecialTags ( allRawText )
1183+ // Extract plan from plan_respond tool call
1184+ const { steps : planSteps , isComplete : planComplete } = extractPlanFromBlocks ( blocks )
1185+ const hasPlan = ! ! ( planSteps && Object . keys ( planSteps ) . length > 0 )
11391186
1140- if ( ! cleanText . trim ( ) && ! allParsed . plan ) return null
1141-
1142- // Check if special tags are present
1143- const hasSpecialTags = ! ! ( allParsed . plan && Object . keys ( allParsed . plan ) . length > 0 )
1187+ if ( ! cleanText . trim ( ) && ! hasPlan ) return null
11441188
11451189 return (
11461190 < div className = 'space-y-1.5' >
@@ -1149,12 +1193,10 @@ function SubAgentThinkingContent({
11491193 content = { cleanText }
11501194 isStreaming = { isStreaming }
11511195 hasFollowingContent = { false }
1152- hasSpecialTags = { hasSpecialTags }
1196+ hasSpecialTags = { hasPlan }
11531197 />
11541198 ) }
1155- { allParsed . plan && Object . keys ( allParsed . plan ) . length > 0 && (
1156- < PlanSteps steps = { allParsed . plan } streaming = { isStreaming } />
1157- ) }
1199+ { hasPlan && < PlanSteps steps = { planSteps } streaming = { ! planComplete } /> }
11581200 </ div >
11591201 )
11601202}
@@ -1235,11 +1277,17 @@ function SubagentContentRenderer({
12351277 segments . push ( { type : 'text' , content : currentText } )
12361278 }
12371279
1238- // Parse plan and options
1280+ // Parse options from text ( plan is extracted from tool call)
12391281 const allParsed = parseSpecialTags ( allRawText )
1282+
1283+ // Extract plan from plan_respond tool call
1284+ const { steps : planSteps , isComplete : planComplete } = extractPlanFromBlocks (
1285+ toolCall . subAgentBlocks
1286+ )
1287+ const hasPlan = ! ! ( planSteps && Object . keys ( planSteps ) . length > 0 )
1288+
12401289 const hasSpecialTags = ! ! (
1241- ( allParsed . plan && Object . keys ( allParsed . plan ) . length > 0 ) ||
1242- ( allParsed . options && Object . keys ( allParsed . options ) . length > 0 )
1290+ hasPlan || ( allParsed . options && Object . keys ( allParsed . options ) . length > 0 )
12431291 )
12441292
12451293 const formatDuration = ( ms : number ) => {
@@ -1251,9 +1299,6 @@ function SubagentContentRenderer({
12511299 const outerLabel = getSubagentCompletionLabel ( toolCall . name )
12521300 const durationText = `${ outerLabel } for ${ formatDuration ( duration ) } `
12531301
1254- // Check if we have a plan to render outside the collapsible
1255- const hasPlan = allParsed . plan && Object . keys ( allParsed . plan ) . length > 0
1256-
12571302 // Render the collapsible content (thinking blocks + tool calls, NOT plan)
12581303 // Inner thinking text always uses "Thought" label
12591304 const renderCollapsibleContent = ( ) => (
@@ -1299,7 +1344,7 @@ function SubagentContentRenderer({
12991344 return (
13001345 < div className = 'w-full space-y-1.5' >
13011346 { renderCollapsibleContent ( ) }
1302- { hasPlan && < PlanSteps steps = { allParsed . plan ! } streaming = { isStreaming } /> }
1347+ { hasPlan && < PlanSteps steps = { planSteps ! } streaming = { ! planComplete } /> }
13031348 </ div >
13041349 )
13051350 }
@@ -1333,7 +1378,7 @@ function SubagentContentRenderer({
13331378 </ div >
13341379
13351380 { /* Plan stays outside the collapsible */ }
1336- { hasPlan && < PlanSteps steps = { allParsed . plan ! } /> }
1381+ { hasPlan && < PlanSteps steps = { planSteps ! } /> }
13371382 </ div >
13381383 )
13391384}
@@ -1958,7 +2003,10 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }:
19582003 toolCall . name === 'checkoff_todo' ||
19592004 toolCall . name === 'mark_todo_in_progress' ||
19602005 toolCall . name === 'tool_search_tool_regex' ||
1961- toolCall . name === 'user_memory'
2006+ toolCall . name === 'user_memory' ||
2007+ toolCall . name === 'edit_responsd' ||
2008+ toolCall . name === 'debug_respond' ||
2009+ toolCall . name === 'plan_respond'
19622010 )
19632011 return null
19642012
0 commit comments