Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,6 @@ import { CLASS_TOOL_METADATA } from '@/stores/panel/copilot/store'
import type { SubAgentContentBlock } from '@/stores/panel/copilot/types'
import { useWorkflowStore } from '@/stores/workflows/workflow/store'

/**
* Parse special tags from content
*/
/**
* Plan step can be either a string or an object with title and plan
*/
Expand All @@ -47,6 +44,56 @@ interface ParsedTags {
cleanContent: string
}

/**
* Extract plan steps from plan_respond tool calls in subagent blocks.
* Returns { steps, isComplete } where steps is in the format expected by PlanSteps component.
*/
function extractPlanFromBlocks(blocks: SubAgentContentBlock[] | undefined): {
steps: Record<string, PlanStep> | undefined
isComplete: boolean
} {
if (!blocks) return { steps: undefined, isComplete: false }

// Find the plan_respond tool call
const planRespondBlock = blocks.find(
(b) => b.type === 'subagent_tool_call' && b.toolCall?.name === 'plan_respond'
)

if (!planRespondBlock?.toolCall) {
return { steps: undefined, isComplete: false }
}

// Tool call arguments can be in different places depending on the source
// Also handle nested data.arguments structure from the schema
const tc = planRespondBlock.toolCall as any
const args = tc.params || tc.parameters || tc.input || tc.arguments || tc.data?.arguments || {}
const stepsArray = args.steps

if (!Array.isArray(stepsArray) || stepsArray.length === 0) {
return { steps: undefined, isComplete: false }
}

// Convert array format to Record<string, PlanStep> format
// From: [{ number: 1, title: "..." }, { number: 2, title: "..." }]
// To: { "1": "...", "2": "..." }
const steps: Record<string, PlanStep> = {}
for (const step of stepsArray) {
if (step.number !== undefined && step.title) {
steps[String(step.number)] = step.title
}
}

// Check if the tool call is complete (not pending/executing)
const isComplete =
planRespondBlock.toolCall.state === ClientToolCallState.success ||
planRespondBlock.toolCall.state === ClientToolCallState.error

return {
steps: Object.keys(steps).length > 0 ? steps : undefined,
isComplete,
}
}

/**
* Try to parse partial JSON for streaming options.
* Attempts to extract complete key-value pairs from incomplete JSON.
Expand Down Expand Up @@ -654,11 +701,20 @@ function SubAgentThinkingContent({
}
}

// Extract plan from plan_respond tool call (preferred) or fall back to <plan> tags
const { steps: planSteps, isComplete: planComplete } = extractPlanFromBlocks(blocks)
const allParsed = parseSpecialTags(allRawText)

if (!cleanText.trim() && !allParsed.plan) return null
// Prefer plan_respond tool data over <plan> tags
const hasPlan =
!!(planSteps && Object.keys(planSteps).length > 0) ||
!!(allParsed.plan && Object.keys(allParsed.plan).length > 0)
const planToRender = planSteps || allParsed.plan
const isPlanStreaming = planSteps ? !planComplete : isStreaming

const hasSpecialTags = !!(allParsed.plan && Object.keys(allParsed.plan).length > 0)
if (!cleanText.trim() && !hasPlan) return null

const hasSpecialTags = hasPlan

return (
<div className='space-y-1.5'>
Expand All @@ -670,9 +726,7 @@ function SubAgentThinkingContent({
hasSpecialTags={hasSpecialTags}
/>
)}
{allParsed.plan && Object.keys(allParsed.plan).length > 0 && (
<PlanSteps steps={allParsed.plan} streaming={isStreaming} />
)}
{hasPlan && planToRender && <PlanSteps steps={planToRender} streaming={isPlanStreaming} />}
</div>
)
}
Expand Down Expand Up @@ -744,8 +798,19 @@ const SubagentContentRenderer = memo(function SubagentContentRenderer({
}

const allParsed = parseSpecialTags(allRawText)

// Extract plan from plan_respond tool call (preferred) or fall back to <plan> tags
const { steps: planSteps, isComplete: planComplete } = extractPlanFromBlocks(
toolCall.subAgentBlocks
)
const hasPlan =
!!(planSteps && Object.keys(planSteps).length > 0) ||
!!(allParsed.plan && Object.keys(allParsed.plan).length > 0)
const planToRender = planSteps || allParsed.plan
const isPlanStreaming = planSteps ? !planComplete : isStreaming

const hasSpecialTags = !!(
(allParsed.plan && Object.keys(allParsed.plan).length > 0) ||
hasPlan ||
(allParsed.options && Object.keys(allParsed.options).length > 0)
)

Expand All @@ -757,8 +822,6 @@ const SubagentContentRenderer = memo(function SubagentContentRenderer({
const outerLabel = getSubagentCompletionLabel(toolCall.name)
const durationText = `${outerLabel} for ${formatDuration(duration)}`

const hasPlan = allParsed.plan && Object.keys(allParsed.plan).length > 0

const renderCollapsibleContent = () => (
<>
{segments.map((segment, index) => {
Expand Down Expand Up @@ -800,7 +863,7 @@ const SubagentContentRenderer = memo(function SubagentContentRenderer({
return (
<div className='w-full space-y-1.5'>
{renderCollapsibleContent()}
{hasPlan && <PlanSteps steps={allParsed.plan!} streaming={isStreaming} />}
{hasPlan && planToRender && <PlanSteps steps={planToRender} streaming={isPlanStreaming} />}
</div>
)
}
Expand Down Expand Up @@ -832,7 +895,7 @@ const SubagentContentRenderer = memo(function SubagentContentRenderer({
</div>

{/* Plan stays outside the collapsible */}
{hasPlan && <PlanSteps steps={allParsed.plan!} />}
{hasPlan && planToRender && <PlanSteps steps={planToRender} />}
</div>
)
})
Expand Down Expand Up @@ -1412,7 +1475,11 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }:
if (
toolCall.name === 'checkoff_todo' ||
toolCall.name === 'mark_todo_in_progress' ||
toolCall.name === 'tool_search_tool_regex'
toolCall.name === 'tool_search_tool_regex' ||
toolCall.name === 'user_memory' ||
toolCall.name === 'edit_responsd' ||
toolCall.name === 'debug_respond' ||
toolCall.name === 'plan_respond'
)
return null

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,13 +209,17 @@ export class SetGlobalWorkflowVariablesClientTool extends BaseClientTool {
}
}

const variablesArray = Object.values(byName)
// Convert byName (keyed by name) to record keyed by ID for the API
const variablesRecord: Record<string, any> = {}
for (const v of Object.values(byName)) {
variablesRecord[v.id] = v
}

// POST full variables array to persist
// POST full variables record to persist
const res = await fetch(`/api/workflows/${payload.workflowId}/variables`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ variables: variablesArray }),
body: JSON.stringify({ variables: variablesRecord }),
})
if (!res.ok) {
const txt = await res.text().catch(() => '')
Expand Down
Loading