feat: add descriptive help text to graph designer properties

Add inline descriptions to workflow settings, orchestration mode,
edge inspector, node inspectors, and condition editor properties
so users understand what each option does at a glance.

- Execution Mode: explains off-thread vs lockstep behavior
- Max Iterations: clarifies it prevents runaway loops
- State Scopes: describes shared state containers
- Edge Kind: explains direct, fan-out, fan-in semantics
- Loop Edge: describes iterative cycle behavior
- Reasoning: explains effort vs answer quality tradeoff
- Triage Agent, Tool-Call Filtering, Max Rounds, etc.
- Result Variable, Require Approval, Port ID, Condition

Also enhances FormField component with optional description prop.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
David Kaya
2026-04-07 09:02:15 +02:00
co-authored by Copilot
parent ad2eab73d7
commit d265db42e5
7 changed files with 101 additions and 54 deletions
+14 -3
View File
@@ -691,6 +691,9 @@ function WorkflowSettingsPanel({
<option value="off-thread">Off-thread</option>
<option value="lockstep">Lockstep</option>
</select>
<p className="text-[11px] leading-relaxed text-[var(--color-text-muted)]">
Off-thread executes in the background without blocking. Lockstep waits for each step to finish before proceeding.
</p>
</label>
<label className="block space-y-1.5">
@@ -712,6 +715,9 @@ function WorkflowSettingsPanel({
type="number"
value={workflow.settings.maxIterations ?? ''}
/>
<p className="text-[11px] leading-relaxed text-[var(--color-text-muted)]">
Safety limit on total step executions to prevent runaway loops.
</p>
</label>
<div className="col-span-2 flex items-center justify-between rounded-lg border border-[var(--color-border)] bg-[var(--color-surface-1)] px-4 py-3">
@@ -790,9 +796,14 @@ function WorkflowSettingsPanel({
{/* State Scopes */}
<div className="col-span-2 space-y-3">
<div className="flex items-center justify-between">
<span className="text-[12px] font-semibold uppercase tracking-wider text-[var(--color-text-muted)]">
State Scopes
</span>
<div>
<span className="text-[12px] font-semibold uppercase tracking-wider text-[var(--color-text-muted)]">
State Scopes
</span>
<p className="mt-0.5 text-[11px] leading-relaxed text-[var(--color-text-muted)]">
Named containers for shared state that persists across workflow steps.
</p>
</div>
<button
className="flex size-6 items-center justify-center rounded-md text-[var(--color-text-muted)] transition-all duration-200 hover:bg-[var(--color-accent)]/10 hover:text-[var(--color-accent)]"
onClick={() => {
+5
View File
@@ -3,10 +3,12 @@ import type { ReactNode } from 'react';
export function FormField({
label,
required,
description,
children,
}: {
label: string;
required?: boolean;
description?: string;
children: ReactNode;
}) {
return (
@@ -16,6 +18,9 @@ export function FormField({
{required && <span className="ml-1 text-[var(--color-status-warning)]">*</span>}
</span>
{children}
{description && (
<p className="mt-1.5 text-[11px] leading-relaxed text-[var(--color-text-muted)]">{description}</p>
)}
</label>
);
}
@@ -241,6 +241,9 @@ export function ConditionEditor({ condition, onChange, disabled }: ConditionEdit
<option value="message-type">Message Type</option>
<option value="expression">Expression</option>
</select>
<p className="text-[11px] leading-relaxed text-[var(--color-text-muted)]">
Determines when this edge fires. Use property rules or expressions to create conditional branches.
</p>
</label>
{condition?.type === 'property' && (
@@ -134,25 +134,35 @@ export function InvokeFunctionInspector({
/>
{/* Result variable */}
<InputField
label="Result Variable"
onChange={(v) => patchConfig({ resultVariable: v || undefined })}
placeholder="e.g. Local.result"
value={config.resultVariable ?? ''}
/>
<div className="space-y-1.5">
<InputField
label="Result Variable"
onChange={(v) => patchConfig({ resultVariable: v || undefined })}
placeholder="e.g. Local.result"
value={config.resultVariable ?? ''}
/>
<p className="text-[11px] leading-relaxed text-[var(--color-text-muted)]">
State path where the return value is stored for use in subsequent steps.
</p>
</div>
{/* Require approval */}
<label className="flex items-center justify-between">
<span className="text-[12px] font-medium text-[var(--color-text-secondary)]">
Require Approval
</span>
<input
checked={config.requireApproval === true}
className="size-4 accent-[var(--color-accent)]"
onChange={(e) => patchConfig({ requireApproval: e.target.checked || undefined })}
type="checkbox"
/>
</label>
<div className="space-y-1">
<label className="flex items-center justify-between">
<span className="text-[12px] font-medium text-[var(--color-text-secondary)]">
Require Approval
</span>
<input
checked={config.requireApproval === true}
className="size-4 accent-[var(--color-accent)]"
onChange={(e) => patchConfig({ requireApproval: e.target.checked || undefined })}
type="checkbox"
/>
</label>
<p className="text-[11px] leading-relaxed text-[var(--color-text-muted)]">
Pause and require human confirmation before invoking this function.
</p>
</div>
{/* Arguments editor */}
<div className="space-y-1.5">
@@ -211,7 +211,7 @@ function HandoffSettingsPanel({
</span>
</div>
<FormField label="Triage Agent">
<FormField label="Triage Agent" description="The entry-point agent that receives requests and decides which specialist to hand off to.">
<SelectInput
value={settings.triageAgentNodeId ?? ''}
options={triageOptions}
@@ -219,7 +219,7 @@ function HandoffSettingsPanel({
/>
</FormField>
<FormField label="Tool-Call Filtering">
<FormField label="Tool-Call Filtering" description="Controls which tools each agent can see and invoke during handoff routing.">
<SelectInput
value={settings.toolCallFiltering}
options={filteringOptions}
@@ -237,7 +237,7 @@ function HandoffSettingsPanel({
</button>
</div>
<FormField label="Custom Handoff Instructions">
<FormField label="Custom Handoff Instructions" description="Additional guidance given to agents when performing handoffs between specialists.">
<textarea
className="w-full rounded-lg border border-[var(--color-border)] bg-[var(--color-surface-2)] px-3 py-2 text-[13px] text-[var(--color-text-primary)] outline-none transition-all duration-200 placeholder:text-[var(--color-text-muted)] focus:border-[var(--color-border-glow)] focus:shadow-[0_0_0_1px_rgba(36,92,249,0.15),0_0_12px_rgba(36,92,249,0.08)]"
rows={3}
@@ -268,7 +268,7 @@ function GroupChatSettingsPanel({
</span>
</div>
<FormField label="Selection Strategy">
<FormField label="Selection Strategy" description="Determines which agent speaks next in each conversation turn.">
<div className="flex items-center gap-2 rounded-lg border border-[var(--color-border)] bg-[var(--color-surface-2)] px-3.5 py-2.5">
<Repeat className="size-3.5 text-[var(--color-text-muted)]" />
<span className="text-[13px] text-[var(--color-text-primary)]">Round Robin</span>
@@ -276,7 +276,7 @@ function GroupChatSettingsPanel({
</div>
</FormField>
<FormField label="Max Rounds">
<FormField label="Max Rounds" description="Maximum number of conversation turns across all agents before the group chat terminates.">
<input
type="number"
min={1}
@@ -101,12 +101,17 @@ export function RequestPortInspector({
/>
{/* Port ID */}
<InputField
label="Port ID"
onChange={(v) => patchConfig({ portId: v })}
placeholder="Unique port identifier"
value={config.portId}
/>
<div className="space-y-1.5">
<InputField
label="Port ID"
onChange={(v) => patchConfig({ portId: v })}
placeholder="Unique port identifier"
value={config.portId}
/>
<p className="text-[11px] leading-relaxed text-[var(--color-text-muted)]">
Unique identifier used to target this port from parent workflows or external callers.
</p>
</div>
{/* Request Type */}
<div className="space-y-1.5">
@@ -155,12 +155,17 @@ function AgentNodeInspector({
}}
value={config.model}
/>
<ReasoningEffortSelect
label="Reasoning"
onChange={(value) => patchConfig({ reasoningEffort: value })}
supportedEfforts={getSupportedReasoningEfforts(model)}
value={config.reasoningEffort}
/>
<div className="space-y-1.5">
<ReasoningEffortSelect
label="Reasoning"
onChange={(value) => patchConfig({ reasoningEffort: value })}
supportedEfforts={getSupportedReasoningEfforts(model)}
value={config.reasoningEffort}
/>
<p className="text-[11px] leading-relaxed text-[var(--color-text-muted)]">
Controls how much the model reasons before responding. Higher values produce more careful, thorough answers.
</p>
</div>
</div>
</div>
);
@@ -290,27 +295,35 @@ function EdgeInspector({
<option value="fan-out">Fan-out</option>
<option value="fan-in">Fan-in</option>
</select>
<p className="text-[11px] leading-relaxed text-[var(--color-text-muted)]">
Direct connects nodes sequentially. Fan-out splits into parallel branches. Fan-in waits for all branches to complete.
</p>
</label>
{/* Loop controls */}
<div className="space-y-2.5">
<label className="flex items-center justify-between">
<span className="text-[12px] font-medium text-[var(--color-text-secondary)]">
Loop Edge
</span>
<input
checked={edge.isLoop === true}
className="size-4 accent-[var(--color-accent)]"
onChange={(e) => {
if (e.target.checked) {
onEdgeChange(edge.id, { isLoop: true, maxIterations: edge.maxIterations ?? 10 });
} else {
onEdgeChange(edge.id, { isLoop: undefined, maxIterations: undefined });
}
}}
type="checkbox"
/>
</label>
<div className="space-y-1">
<label className="flex items-center justify-between">
<span className="text-[12px] font-medium text-[var(--color-text-secondary)]">
Loop Edge
</span>
<input
checked={edge.isLoop === true}
className="size-4 accent-[var(--color-accent)]"
onChange={(e) => {
if (e.target.checked) {
onEdgeChange(edge.id, { isLoop: true, maxIterations: edge.maxIterations ?? 10 });
} else {
onEdgeChange(edge.id, { isLoop: undefined, maxIterations: undefined });
}
}}
type="checkbox"
/>
</label>
<p className="text-[11px] leading-relaxed text-[var(--color-text-muted)]">
Creates an iterative cycle, re-executing the path between these nodes up to a set limit.
</p>
</div>
{edge.isLoop && (
<label className="block space-y-1.5">
<span className="text-[11px] text-[var(--color-text-muted)]">Max Iterations</span>