Compare commits

...

3 Commits

Author SHA1 Message Date
xingyue b370d96504 fix(dashboard): alternate feedback edges left/right (#247 Phase 2)
What: Feedback (back) edges now alternate between left and right sides
instead of all routing to the right.

Why: Multiple feedback edges targeting the same node (e.g. reviewer→coder
and tester→coder) were overlapping on the right side.

Changes:
- types.ts: add feedbackSide field to ConditionEdgeData
- use-layout.ts: track feedback count per target, alternate sides
- condition-edge.tsx: feedbackPath() accepts side param, mirrors path for left

Ref: #247, closes #249
2026-05-14 11:42:06 +08:00
xingyue 8cae114c7e fix(dashboard): unified solid edges, hide FALLBACK labels, conditional cursor (#247 Phase 1)
What: Restore graph visual preferences — all edges solid, FALLBACK labels hidden,
inactive nodes not clickable.

Why: Visual consistency and cleaner graph appearance per design preferences.

Changes:
- condition-edge.tsx: remove strokeDasharray, unify stroke color, hide FALLBACK labels
- role-node.tsx: cursor-pointer only on non-default state nodes

Ref: #247, closes #248
2026-05-14 11:39:51 +08:00
xiaomo 9b23e6f85a Merge pull request 'refactor(serve): remove tunnel + eliminate HTTP round-trip in gateway mode' (#245) from refactor/serve-remove-http-tunnel into main 2026-05-13 15:29:05 +00:00
4 changed files with 38 additions and 27 deletions
@@ -7,25 +7,23 @@ const FEEDBACK_OFFSET_X = 100;
const FEEDBACK_RADIUS = 16;
/**
* Build an SVG path for a feedback (back) edge that routes to the right of the nodes.
* The path goes: source right → arc → vertical up → arc → target right
* Build an SVG path for a feedback (back) edge that routes to the given side of the nodes.
* The path goes: source → arc → vertical up → arc → target
*/
function feedbackPath(sourceX: number, sourceY: number, targetX: number, targetY: number): string {
const rightX = Math.max(sourceX, targetX) + FEEDBACK_OFFSET_X;
function feedbackPath(sourceX: number, sourceY: number, targetX: number, targetY: number, side: "right" | "left"): string {
const d = side === "right" ? 1 : -1;
const offsetX =
side === "right"
? Math.max(sourceX, targetX) + FEEDBACK_OFFSET_X
: Math.min(sourceX, targetX) - FEEDBACK_OFFSET_X;
const r = FEEDBACK_RADIUS;
// Start from source right side, go right, then up, then left to target right side
const segments = [
`M ${sourceX} ${sourceY}`,
// Horizontal to the right
`L ${rightX - r} ${sourceY}`,
// Arc turning upward
`Q ${rightX} ${sourceY} ${rightX} ${sourceY - r}`,
// Vertical upward
`L ${rightX} ${targetY + r}`,
// Arc turning left
`Q ${rightX} ${targetY} ${rightX - r} ${targetY}`,
// Horizontal left to target
`L ${offsetX - d * r} ${sourceY}`,
`Q ${offsetX} ${sourceY} ${offsetX} ${sourceY - r}`,
`L ${offsetX} ${targetY + r}`,
`Q ${offsetX} ${targetY} ${offsetX - d * r} ${targetY}`,
`L ${targetX} ${targetY}`,
];
@@ -57,10 +55,13 @@ export function ConditionEdge(props: EdgeProps) {
let defaultLabelY: number;
if (isFeedback) {
// Custom feedback path routed to the right
path = feedbackPath(sourceX, sourceY, targetX, targetY);
const rightX = Math.max(sourceX, targetX) + FEEDBACK_OFFSET_X;
defaultLabelX = rightX;
const side = edgeData?.feedbackSide ?? "right";
path = feedbackPath(sourceX, sourceY, targetX, targetY, side);
const offsetX =
side === "right"
? Math.max(sourceX, targetX) + FEEDBACK_OFFSET_X
: Math.min(sourceX, targetX) - FEEDBACK_OFFSET_X;
defaultLabelX = offsetX;
defaultLabelY = (sourceY + targetY) / 2;
} else {
const result = getSmoothStepPath({
@@ -78,9 +79,8 @@ export function ConditionEdge(props: EdgeProps) {
defaultLabelY = result[2];
}
const stroke = isFallback ? "var(--color-text-muted)" : "var(--color-accent)";
const strokeDasharray = isFallback ? "5 4" : undefined;
const label = edgeData?.condition ?? "";
const stroke = "var(--color-accent)";
const label = isFallback ? "" : (edgeData?.condition ?? "");
// Use pre-computed label position if available, otherwise fall back to default
const labelX = edgeData?.labelX ?? defaultLabelX;
@@ -92,7 +92,7 @@ export function ConditionEdge(props: EdgeProps) {
id={id}
path={path}
markerEnd={markerEnd}
style={{ stroke, strokeWidth: 1.5, strokeDasharray }}
style={{ stroke, strokeWidth: 1.5 }}
/>
{label !== "" && (
<EdgeLabelRenderer>
@@ -102,7 +102,7 @@ export function ConditionEdge(props: EdgeProps) {
transform: `translate(-50%, -50%) translate(${labelX}px, ${labelY}px)`,
background: "var(--color-surface)",
border: "1px solid var(--color-border)",
color: isFallback ? "var(--color-text-muted)" : "var(--color-text)",
color: "var(--color-text)",
whiteSpace: "nowrap",
zIndex: 10,
}}
@@ -31,7 +31,7 @@ export function RoleNode(props: NodeProps) {
return (
<div
className={`px-3 py-2 rounded-md border-2 text-xs font-medium cursor-pointer ${isActive ? "wf-node-pulse" : ""}`}
className={`px-3 py-2 rounded-md border-2 text-xs font-medium ${t.state !== "default" ? "cursor-pointer" : ""} ${isActive ? "wf-node-pulse" : ""}`}
style={{
width: 180,
height: 60,
@@ -23,6 +23,7 @@ export type ConditionEdgeData = {
isFallback: boolean;
isFeedback: boolean;
isSelfLoop: boolean;
feedbackSide: "right" | "left" | null;
labelX: number | null;
labelY: number | null;
[key: string]: unknown;
@@ -173,6 +173,8 @@ function computeLayout(input: LayoutInput): LayoutResult {
// Build edges with label positions
// For feedback edges (target rank < source rank), we'll compute label at midpoint
// of the right-side arc. The actual SVG path is drawn by ConditionEdge component.
// Track feedback edge count per target node for alternating sides
const feedbackCountByTarget = new Map<string, number>();
const edges: Edge[] = input.edges.map((e) => {
const isFallback = e.condition === "FALLBACK";
const isSelfLoop = e.from === e.to;
@@ -185,13 +187,20 @@ function computeLayout(input: LayoutInput): LayoutResult {
let labelX: number | null = null;
let labelY: number | null = null;
let feedbackSide: "right" | "left" | null = null;
if (sourcePos !== undefined && targetPos !== undefined) {
if (isFeedback) {
// Label on the right side of the feedback arc
const rightX = centerX + ROLE_NODE_WIDTH / 2 + FEEDBACK_OFFSET_X;
// Alternate feedback edges left/right per target node
const count = feedbackCountByTarget.get(e.to) ?? 0;
feedbackCountByTarget.set(e.to, count + 1);
feedbackSide = count % 2 === 0 ? "right" : "left";
const offsetX =
feedbackSide === "right"
? centerX + ROLE_NODE_WIDTH / 2 + FEEDBACK_OFFSET_X
: centerX - ROLE_NODE_WIDTH / 2 - FEEDBACK_OFFSET_X;
const midY = (sourcePos.y + sourcePos.h / 2 + targetPos.y + targetPos.h / 2) / 2;
labelX = rightX;
labelX = offsetX;
labelY = midY;
} else if (!isSelfLoop) {
// Forward edge: label between source bottom and target top
@@ -214,6 +223,7 @@ function computeLayout(input: LayoutInput): LayoutResult {
isFallback,
isFeedback,
isSelfLoop,
feedbackSide,
labelX,
labelY,
},