Skip to content

feat: fixes routing rules tree view with better layout and node UI#2407

Merged
Pratham-Mishra04 merged 1 commit intov1.5.0from
03-30-feat_fixes_routing_rules_tree_view_with_better_layout_and_node_ui
Apr 2, 2026
Merged

feat: fixes routing rules tree view with better layout and node UI#2407
Pratham-Mishra04 merged 1 commit intov1.5.0from
03-30-feat_fixes_routing_rules_tree_view_with_better_layout_and_node_ui

Conversation

@roroghost17
Copy link
Copy Markdown
Contributor

@roroghost17 roroghost17 commented Mar 30, 2026

Summary

Implements a new routing rules tree visualization that displays routing rules as an interactive directed graph. The tree view shows the logical flow from incoming requests through conditions to rule execution, with support for chain rules that loop back into the evaluation process.

Changes

  • Backend: Removed omitempty from Targets field in TableRoutingRule to ensure targets are always serialized
  • Frontend: Complete routing tree visualization implementation including:
    • CEL (Common Expression Language) parser for evaluating routing conditions
    • Trie-based graph construction that merges duplicate condition paths
    • Dagre-based automatic layout with left-to-right flow
    • Interactive React Flow graph with custom node types (source, condition, rule)
    • Chain rule visualization showing feedback loops with static vs dynamic analysis
    • Node selection and path highlighting
    • Search functionality with rule filtering
    • Position persistence across sessions
    • Scope-based color coding for different rule types

Type of change

  • Feature

Affected areas

  • Core (Go)
  • UI (Next.js)

How to test

# Core/Transports
go version
go test ./...

# UI
cd ui
pnpm i || npm i
pnpm test || npm test
pnpm build || npm run build

Navigate to the routing rules page and click the tree view tab to see the new visualization. Create routing rules with different scopes and CEL expressions to test the graph generation and layout.

Screenshots/Recordings

The new tree view provides an interactive graph showing:

  • Source node representing incoming requests
  • Condition nodes displaying CEL expressions with scope indicators
  • Rule nodes showing targets and chain rule indicators
  • Chain edges with different styles for static vs dynamic analysis
  • Search and selection highlighting
  • Automatic layout with position persistence

Breaking changes

  • Yes
  • No

Related issues

Implements routing rules tree visualization feature request.

Security considerations

No security implications - this is a read-only visualization of existing routing rule data.

Checklist

  • I read docs/contributing/README.md and followed the guidelines
  • I added/updated tests where appropriate
  • I updated documentation where needed
  • I verified builds succeed (Go and UI)
  • I verified the CI pipeline passes locally if applicable

@CLAassistant
Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 30, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b149bbdd-47d7-49aa-b148-f847a9f45855

📥 Commits

Reviewing files that changed from the base of the PR and between e36f409 and 22f1a40.

⛔ Files ignored due to path filters (1)
  • ui/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (14)
  • framework/configstore/tables/routing_rules.go
  • ui/app/globals.css
  • ui/app/workspace/routing-rules/tree/views/celParser.ts
  • ui/app/workspace/routing-rules/tree/views/constants.ts
  • ui/app/workspace/routing-rules/tree/views/graphBuilder.ts
  • ui/app/workspace/routing-rules/tree/views/node/rfConditionNode.tsx
  • ui/app/workspace/routing-rules/tree/views/node/rfEdgeHandle.tsx
  • ui/app/workspace/routing-rules/tree/views/node/rfRuleNode.tsx
  • ui/app/workspace/routing-rules/tree/views/node/rfSourceNode.tsx
  • ui/app/workspace/routing-rules/tree/views/positionPersistence.ts
  • ui/app/workspace/routing-rules/tree/views/rfChainEdge.tsx
  • ui/app/workspace/routing-rules/tree/views/routingTreeView.tsx
  • ui/components/ui/asyncMultiselect.tsx
  • ui/package.json

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Interactive routing rules tree with selectable/highlightable nodes/edges and a “Reset layout” action.
  • UI Components

    • New source / condition / rule nodes, chain-back edges and edge-handle; graph builder pipeline; CEL condition parsing/evaluation; persistent layout/positioning.
  • Removed

    • EvaluatorMultiSelect component removed.
  • Style

    • Animated dynamic-chain edge styling with reduced-motion support.
  • Bug Fixes

    • Routing rule JSON now always includes the targets property.

Walkthrough

Extracts routing-tree logic into dedicated frontend modules (CEL parsing, trie/DAG graph construction, layout, React Flow nodes/edges, position persistence), refactors routingTreeView to compose them with selection/highlighting and layout persistence, and changes a backend JSON tag so targets is always emitted.

Changes

Cohort / File(s) Summary
Backend Model Update
framework/configstore/tables/routing_rules.go
Changed TableRoutingRule.Targets JSON tag from json:"targets,omitempty"json:"targets" so targets is always emitted during JSON serialization; GORM tag and hooks unchanged.
CEL & Condition Utilities
ui/app/workspace/routing-rules/tree/views/celParser.ts
New CEL helpers: evalChainCondition, expandCEL, normalizeCond — pattern-limited evaluation, DNF-like expansion, and stable condition normalization.
UI Constants & Layout Config
ui/app/workspace/routing-rules/tree/views/constants.ts
New SCOPE_CONFIG, ScopeKey, SCOPE_ORDER, and layout/Dagre/Fit constants for routing-tree rendering.
Graph Construction Pipeline
ui/app/workspace/routing-rules/tree/views/graphBuilder.ts
New pipeline: build trie from expanded conditions, frequency-sorted insertion, subtree merging (structural dedupe), DAG collection (scope-weighted colors, terminals, chain-back collection), LR layout via Dagre (excluding chain-back), and mapping to React Flow nodes/edges.
React Flow Nodes
ui/app/workspace/routing-rules/tree/views/node/rfSourceNode.tsx, .../rfConditionNode.tsx, .../rfRuleNode.tsx
Added RFSourceNode, RFConditionNode, and RFRuleNode components with sized layouts, handles, scope badges, chain indicators, hover popovers, and target summaries.
Edge Handle & Chain Edge
ui/app/workspace/routing-rules/tree/views/node/rfEdgeHandle.tsx, .../rfChainEdge.tsx
Added RFEdgeHandle (fixed 14px handle with optional accent) and RfChainEdge (looping cubic Bézier chain-back edge with midpoint marker and chainWeak styling).
Position Persistence
ui/app/workspace/routing-rules/tree/views/positionPersistence.ts
New persistence contract: POSITIONS_COOKIE, PositionCookie schema, and computeFingerprint(rules) using sorted id:updated_at entries.
Routing Tree View Refactor
ui/app/workspace/routing-rules/tree/views/routingTreeView.tsx
Replaced monolith with imports from new modules; added edgeTypes (includes rfChain), React Flow instance capture, reset-layout action, selection/edge selection state, selection-overrides-highlighting, position cookie handling, updated toolbar/legend, and interaction handlers.
Shared Component Cleanup
ui/components/ui/asyncMultiselect.tsx
Removed EvaluatorMultiSelect API and evaluator-specific rendering/filtering; reduced min menu height (400 → 160); removed unused imports.
Dependencies & Styling
ui/package.json, ui/app/globals.css
Added dependency @dagrejs/dagre@3.0.0; added keyframes and classes for animated dashed dynamic chain edges with prefers-reduced-motion override.

Sequence Diagram

sequenceDiagram
    participant UI as routingTreeView (Client)
    participant Parser as CEL Parser
    participant Trie as Trie Builder
    participant Merger as Subtree Merger
    participant DAG as DAG Collector
    participant Layout as Dagre Layout
    participant RF as React Flow Renderer

    UI->>Parser: expandCEL / normalizeCond for rules
    Parser->>Trie: insert normalized condition paths
    Trie->>Trie: attach deduplicated rule terminals
    Trie->>Merger: provide root trie
    Merger->>DAG: produce layered DAG structure
    DAG->>Parser: evalChainCondition for chain rules
    DAG->>Layout: send nodes+edges (exclude chain-back edges)
    Layout->>RF: positioned nodes & edges
    DAG->>RF: add chain-back edges (weak/strong) and metadata
    RF->>UI: render RFSource / RFCondition / RFRule nodes + RfChainEdge
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • danpiths
  • akshaydeo

"I hop the branches, parse each little gate,
I merge the tunnels and tidy every state.
Chains curl back kindly, nodes gleam in a row,
Positions remembered where soft carrots grow—
A small rabbit cheers for the code you made so neat."

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 51.43% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main feature: implementing routing rules tree view with improved layout and node UI components.
Description check ✅ Passed The PR description comprehensively covers purpose, implementation details, affected areas, testing instructions, breaking changes, and security considerations per template.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 03-30-feat_fixes_routing_rules_tree_view_with_better_layout_and_node_ui

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor Author

roroghost17 commented Mar 30, 2026

@roroghost17 roroghost17 mentioned this pull request Mar 30, 2026
18 tasks
@roroghost17 roroghost17 force-pushed the 03-30-feat_fixes_routing_rules_tree_view_with_better_layout_and_node_ui branch from 598f05b to e6396bd Compare March 30, 2026 18:57
@roroghost17 roroghost17 force-pushed the 03-26-feat_adds_routing_tree_view branch from 6081883 to c881ec9 Compare March 30, 2026 18:57
@roroghost17 roroghost17 marked this pull request as ready for review March 30, 2026 18:58
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Mar 30, 2026

Confidence Score: 5/5

  • Safe to merge; all remaining findings are P2 quality/edge-case concerns that do not block the primary user path.
  • All P0/P1 concerns from prior review threads have been resolved. The two new findings (500-rule silent truncation, CEL Cartesian blowup) are realistic edge cases but won't affect typical usage, and neither causes data loss or a broken primary path.
  • routingTreeView.tsx (500-rule cap) and celParser.ts (Cartesian expansion) are worth a follow-up in a subsequent PR.

Important Files Changed

Filename Overview
framework/configstore/tables/routing_rules.go Removes omitempty from Targets, ensuring the field always serialises as [] rather than being omitted — required so the frontend can safely iterate rule.targets.
ui/app/workspace/routing-rules/tree/views/celParser.ts CEL parser with splitOn, expandCEL, normalizeCond, and evalChainCondition; Cartesian-product expansion can grow exponentially for deeply nested OR-AND expressions.
ui/app/workspace/routing-rules/tree/views/graphBuilder.ts Trie construction, subtree merging, DAG traversal, Dagre layout, and React Flow graph assembly — well-structured with proper cycle guards; the 500-rule cap in the caller leaves the graph silently incomplete for large workspaces.
ui/app/workspace/routing-rules/tree/views/routingTreeView.tsx Main React Flow component with selection, search, position persistence, and chain-edge legend; timeout cleanup is properly handled; hard limit: 500 silently truncates large graphs.
ui/app/workspace/routing-rules/tree/views/rfChainEdge.tsx Custom cubic Bézier chain-back edge with correct outward-loop control points, mid-path arrow, and memo wrapping.
ui/components/ui/asyncMultiselect.tsx Removes the unused EvaluatorMultiSelect component (not referenced elsewhere) and lowers minMenuHeight from 400 to 160; no issues.

Reviews (6): Last reviewed commit: "feat: fixes routing rules tree view with..." | Re-trigger Greptile

Comment thread ui/app/workspace/routing-rules/tree/views/node/rfConditionNode.tsx Outdated
Comment thread ui/app/workspace/routing-rules/tree/views/celParser.ts
Comment thread ui/app/workspace/routing-rules/tree/views/routingTreeView.tsx Outdated
Comment thread ui/app/workspace/routing-rules/tree/views/positionPersistence.ts
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (11)
ui/app/workspace/routing-rules/tree/views/node/rfConditionNode.tsx (1)

11-15: Consider adding type safety for node data.

The data: any type loses type safety. Since the graph builder controls the data shape, consider defining an interface:

♻️ Optional: Add typed data interface
+interface ConditionNodeData {
+	condition: string;
+	color?: string | null;
+	scopes?: string[];
+}
+
-export function RFConditionNode({ data }: { data: any }) {
-	const condition = data.condition as string;
-	const color = data.color as string | null;
-	const scopes = (data.scopes as string[] | undefined) ?? [];
+export function RFConditionNode({ data }: { data: ConditionNodeData }) {
+	const { condition, color = null, scopes = [] } = data;
 	const accent = color ?? undefined;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/app/workspace/routing-rules/tree/views/node/rfConditionNode.tsx` around
lines 11 - 15, The component RFConditionNode currently types props as data: any,
losing type safety; define a specific interface (e.g., RFConditionNodeData) that
describes condition: string, color?: string | null, scopes?: string[] and use it
in the component signature (RFConditionNode({ data }: { data:
RFConditionNodeData }) or a typed prop) and update usages of condition, color,
scopes, accent to rely on that interface instead of casting—this will ensure
compile-time checks and safer refactors for RFConditionNode and any callers that
construct its data.
ui/app/workspace/routing-rules/tree/views/node/rfEdgeHandle.tsx (1)

7-7: Exported constant not used in component styles.

RF_HANDLE_SIZE_PX is exported (presumably for external calculations) but the actual sizing in line 19 uses hardcoded 14px values. Consider using a CSS custom property or inline style to keep them in sync:

♻️ Optional: Use the constant for sizing
 export function RFEdgeHandle({ className, accentColor, style, ...rest }: RFEdgeHandleProps) {
 	return (
 		<Handle
 			className={cn(
 				"!z-0 !pointer-events-auto",
-				"!h-[14px] !w-[14px] !min-h-[14px] !min-w-[14px]",
+				"!rounded-full !border-0 !border-none !p-0 !shadow-none",
 				"!rounded-full !border-0 !border-none !p-0 !shadow-none",
 				className,
 			)}
-			style={{
-				...style,
-				...(accentColor ? { background: accentColor } : {}),
-			}}
+			style={{
+				width: RF_HANDLE_SIZE_PX,
+				height: RF_HANDLE_SIZE_PX,
+				minWidth: RF_HANDLE_SIZE_PX,
+				minHeight: RF_HANDLE_SIZE_PX,
+				...style,
+				...(accentColor ? { background: accentColor } : {}),
+			}}
 			{...rest}
 		/>
 	);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/app/workspace/routing-rules/tree/views/node/rfEdgeHandle.tsx` at line 7,
The exported RF_HANDLE_SIZE_PX constant is not used in the component styles;
replace the hardcoded "14px" values inside the RFEdgeHandle component (or
related style block in rfEdgeHandle.tsx) with a single source of truth by either
setting a CSS custom property (e.g. --rf-handle-size) to
`${RF_HANDLE_SIZE_PX}px` on the element or by applying an inline style that uses
RF_HANDLE_SIZE_PX to compute width/height/border-radius, so sizing stays in sync
with the exported constant.
ui/app/workspace/routing-rules/tree/views/routingTreeView.tsx (4)

61-62: Consider using the ReactFlowInstance type.

The any type with eslint-disable could be replaced with the proper type from @xyflow/react.

♻️ Suggested type improvement
+import type { ReactFlowInstance } from "@xyflow/react";
...
-	// eslint-disable-next-line `@typescript-eslint/no-explicit-any`
-	const rfInstanceRef = useRef<any>(null);
+	const rfInstanceRef = useRef<ReactFlowInstance | null>(null);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/app/workspace/routing-rules/tree/views/routingTreeView.tsx` around lines
61 - 62, Replace the any-typed ref by importing and using the proper
ReactFlowInstance type from '@xyflow/react' and remove the eslint-disable;
specifically change the rfInstanceRef declaration (currently const rfInstanceRef
= useRef<any>(null)) to useRef<ReactFlowInstance | null>(null) and add the
corresponding import for ReactFlowInstance so the ref is strongly typed
throughout the component.

399-405: Nested ternary reduces readability.

The nested ternary for displaying rule count could be extracted for clarity.

♻️ Suggested extraction
+	const ruleCountText = useMemo(() => {
+		if (!search) return `${rules.length} rule${rules.length !== 1 ? "s" : ""}`;
+		if (highlightedIds && highlightedIds.size > 0) {
+			return `${matchCount} rule${matchCount !== 1 ? "s" : ""}`;
+		}
+		return "no match";
+	}, [search, highlightedIds, matchCount, rules.length]);
...
							<p className="text-[11px] text-muted-foreground">
-								{search
-										? highlightedIds && highlightedIds.size > 0
-											? `${matchCount} rule${matchCount !== 1 ? "s" : ""}`
-											: "no match"
-										: `${rules.length} rule${rules.length !== 1 ? "s" : ""}`}
+								{ruleCountText}
							</p>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/app/workspace/routing-rules/tree/views/routingTreeView.tsx` around lines
399 - 405, The nested ternary in the JSX reduces readability; extract the logic
that builds the display string into a small helper/variable (e.g.,
computeRuleCountText or ruleCountText) inside the RoutingTreeView component and
return a single string to render. Implement the helper to accept/close over
search, highlightedIds, matchCount and rules.length and produce the three
possible outputs ("no match", "{matchCount} rule(s)", or "{rules.length}
rule(s)") with proper pluralization, then replace the nested ternary in the <p>
with that single variable or function call.

180-186: Chain edge identification is coupled to ID naming convention.

The chain-back edge exclusion relies on e.id.endsWith("-chain"), which creates a tight coupling between this component and the edge ID format in graphBuilder.ts. Consider using a data property instead for more explicit identification.

♻️ Suggested approach

In graphBuilder.ts, the edge already has isChainBack during construction. You could add a data.isChainBack property to the final edge:

// In graphBuilder.ts rfEdges mapping
data: { chainWeak: weak, isChainBack: true },

Then in routingTreeView.tsx:

-			if (!e.id.endsWith("-chain")) {
+			if (!(e.data as { isChainBack?: boolean })?.isChainBack) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/app/workspace/routing-rules/tree/views/routingTreeView.tsx` around lines
180 - 186, The code in routingTreeView.tsx currently treats chain-back edges by
checking e.id.endsWith("-chain"), which couples this view to the edge ID format;
update the graph construction to set a canonical flag (e.g., add
data.isChainBack = true in the rfEdges mapping inside graphBuilder.ts), then
change the exclusion logic in routingTreeView.tsx to check that flag (e.g., if
(!e.data?.isChainBack) { ... }) when populating parentsOf/childrenOf; update any
references to e.id-based logic (the current e.id.endsWith("-chain") check) to
use e.data.isChainBack so identification is explicit and decoupled from naming.

147-155: Verify timeout is necessary for fitView.

The 50ms setTimeout delay before calling fitView suggests a potential race condition with React state updates. This works but is fragile. Consider using requestAnimationFrame or ensuring the state flush completes before fitting.

♻️ Alternative approach
-		setTimeout(() => rfInstanceRef.current?.fitView({ padding: FIT_VIEW_PADDING, duration: 300 }), 50);
+		requestAnimationFrame(() => {
+			rfInstanceRef.current?.fitView({ padding: FIT_VIEW_PADDING, duration: 300 });
+		});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/app/workspace/routing-rules/tree/views/routingTreeView.tsx` around lines
147 - 155, The setTimeout used in handleResetLayout to call
rfInstanceRef.current?.fitView is fragile; replace it with a reliable
post-render callback such as requestAnimationFrame (or ReactDOM.flushSync +
immediate call) so fitView runs after the state updates complete. Update
handleResetLayout to clear cookies and update cookieDataRef/current state
(cookieDataRef, setNodes, setSelectedNodeId, setSelectedEdgeId) as before, then
call rfInstanceRef.current?.fitView from inside requestAnimationFrame (or from
an effect that watches baseNodes/nodes) to ensure layout fitting happens after
React has flushed the DOM; reference handleResetLayout, rfInstanceRef,
cookieDataRef, setNodes, setSelectedNodeId, and setSelectedEdgeId when locating
the change.
ui/app/workspace/routing-rules/tree/views/node/rfRuleNode.tsx (2)

13-18: Consider adding type safety for the node data prop.

The data: any type loses type safety. Consider defining an interface for the node data structure.

♻️ Suggested type definition
+interface RFRuleNodeData {
+	rule: RoutingRule;
+	scopeColor: string;
+}
+
-export function RFRuleNode({ data }: { data: any }) {
-	const rule = data.rule as RoutingRule;
-	const scopeColor = data.scopeColor as string;
+export function RFRuleNode({ data }: { data: RFRuleNodeData }) {
+	const { rule, scopeColor } = data;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/app/workspace/routing-rules/tree/views/node/rfRuleNode.tsx` around lines
13 - 18, The RFRuleNode component currently types its props as any which loses
type safety; define a specific interface (e.g., RFRuleNodeData or
RFRuleNodeProps) describing the shape returned in data — include fields like
rule: RoutingRule, scopeColor: string, and any other used members (e.g.,
rule.scope as ScopeKey and rule.targets: array) — then change the component
signature to use that interface (export function RFRuleNode({ data }:
RFRuleNodeProps) or similar) so usages of SCOPE_CONFIG, RoutingRule, ScopeKey,
data.rule, data.scopeColor and rule.targets are type-checked.

80-133: Hover popover may cause layout shifts or overflow issues.

The popover is positioned with left-full ml-3, which could cause horizontal overflow when the node is near the right edge of the viewport. Consider adding boundary detection or using a tooltip library that handles positioning automatically.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/app/workspace/routing-rules/tree/views/node/rfRuleNode.tsx` around lines
80 - 133, The popover anchored in the hovered block (when hovered is true) uses
fixed classes left-full and ml-3 which can overflow the viewport; update the
component to detect available space and flip or clamp the popover instead of
always using left-full: add a small local state (e.g., popoverSide or
popoverOffset) and compute placement in an effect using the node element's
getBoundingClientRect (or swap in a positioning library like Popper.js), then
conditionally apply classes (replace left-full ml-3 with right/full or a
max-width & translate-x clamp) while still using rule, scopeColor and
RenderProviderIcon as is so the visual content remains identical.
ui/app/workspace/routing-rules/tree/views/graphBuilder.ts (2)

55-56: Consider adding proper types for LNode and LEdge.

The data: any in LNode and the unstructured LEdge interface reduce type safety. Since these are internal types, consider defining stricter types for better maintainability.

♻️ Suggested type definitions
-interface LNode { id: string; kind: "source" | "condition" | "rule" | "target"; data: any; w: number; h: number; }
-interface LEdge { source: string; target: string; label?: string; color?: string; isChainBack?: boolean; isChainWeak?: boolean; sourceHandle?: string; targetHandle?: string; }
+interface LNodeData {
+	condition?: string;
+	color?: string | null;
+	scopes?: string[];
+	rule?: RoutingRule;
+	scopeColor?: string;
+}
+
+interface LNode {
+	id: string;
+	kind: "source" | "condition" | "rule" | "target";
+	data: LNodeData;
+	w: number;
+	h: number;
+}
+
+interface LEdge {
+	source: string;
+	target: string;
+	label?: string;
+	color?: string;
+	isChainBack?: boolean;
+	isChainWeak?: boolean;
+	sourceHandle?: string;
+	targetHandle?: string;
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/app/workspace/routing-rules/tree/views/graphBuilder.ts` around lines 55 -
56, The LNode and LEdge types are too loose; replace data: any in LNode with a
discriminated union keyed by kind (e.g., SourceNodeData, ConditionNodeData,
RuleNodeData, TargetNodeData) so each node kind exposes only the fields it
needs, and tighten LEdge by explicitly typing optional fields (label: string,
color: string, isChainBack: boolean, isChainWeak: boolean, sourceHandle?:
string, targetHandle?: string) and any metadata shape used in edges; update any
code referencing LNode or LEdge (e.g., graphBuilder functions that
create/inspect nodes and edges) to use the new union members and adjust pattern
matches/field access accordingly to satisfy the stricter types.

91-95: Duplicate rule check has O(n) complexity per insertion.

The find call on line 94 scans the terminals array for each insertion. For rules with many paths, this could be slow. Consider using a Set for O(1) lookup.

♻️ Suggested optimization
 export interface TrieNode {
 	id: string;
 	condition: string | null;
 	children: Map<string, TrieNode>;
 	terminals: RoutingRule[];
+	terminalIds: Set<string>;
 }
...
 	const mkNode = (c: string | null): TrieNode =>
-		({ id: c === null ? "root" : `n${++uid}`, condition: c, children: new Map(), terminals: [] });
+		({ id: c === null ? "root" : `n${++uid}`, condition: c, children: new Map(), terminals: [], terminalIds: new Set() });
...
-		if (!node.terminals.find((r) => r.id === rule.id)) node.terminals.push(rule);
+		if (!node.terminalIds.has(rule.id)) {
+			node.terminalIds.add(rule.id);
+			node.terminals.push(rule);
+		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/app/workspace/routing-rules/tree/views/graphBuilder.ts` around lines 91 -
95, The current insertion into the trie uses node.terminals.find((r) => r.id ===
rule.id) which is O(n) per insert; change node.terminals to maintain an O(1)
lookup (e.g., add a node.terminalIds: Set<string> alongside node.terminals array
or replace terminals with a Map/Set) and update the insertion logic in the graph
builder (the block using mkNode, node.children, node.terminals, and rule.id) to
check terminalIds.has(rule.id) before pushing to node.terminals and to add
rule.id to terminalIds when inserting so duplicate checks become O(1).
ui/components/ui/asyncMultiselect.tsx (1)

459-459: Consider making the 160px menu height opt-in instead of the shared default.

AsyncMultiSelect is a base UI component, so this changes every caller, not just the new routing-rules tree. If the tighter menu is only needed for that flow, thread minMenuHeight through props and set 160 at the callsite instead of hard-coding it here.

As per coding guidelines, **: always check the stack if there is one for the current PR. do not give localized reviews for the PR, always see all changes in the light of the whole stack of PRs (if there is a stack, if there is no stack you can continue to make localized suggestions/reviews)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/components/ui/asyncMultiselect.tsx` at line 459, The hard-coded
minMenuHeight=160 in AsyncMultiSelect should be made configurable: add an
optional prop (e.g., minMenuHeight?: number) to the AsyncMultiSelect
props/interface and replace the literal 160 with the prop value (use the prop
when provided, otherwise fall back to the component's existing default behavior
or undefined), then update the routing-rules tree callsite to pass
minMenuHeight={160} where the tighter menu is required; reference
AsyncMultiSelect and the minMenuHeight prop in your changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@ui/app/workspace/routing-rules/tree/views/graphBuilder.ts`:
- Around line 55-56: The LNode and LEdge types are too loose; replace data: any
in LNode with a discriminated union keyed by kind (e.g., SourceNodeData,
ConditionNodeData, RuleNodeData, TargetNodeData) so each node kind exposes only
the fields it needs, and tighten LEdge by explicitly typing optional fields
(label: string, color: string, isChainBack: boolean, isChainWeak: boolean,
sourceHandle?: string, targetHandle?: string) and any metadata shape used in
edges; update any code referencing LNode or LEdge (e.g., graphBuilder functions
that create/inspect nodes and edges) to use the new union members and adjust
pattern matches/field access accordingly to satisfy the stricter types.
- Around line 91-95: The current insertion into the trie uses
node.terminals.find((r) => r.id === rule.id) which is O(n) per insert; change
node.terminals to maintain an O(1) lookup (e.g., add a node.terminalIds:
Set<string> alongside node.terminals array or replace terminals with a Map/Set)
and update the insertion logic in the graph builder (the block using mkNode,
node.children, node.terminals, and rule.id) to check terminalIds.has(rule.id)
before pushing to node.terminals and to add rule.id to terminalIds when
inserting so duplicate checks become O(1).

In `@ui/app/workspace/routing-rules/tree/views/node/rfConditionNode.tsx`:
- Around line 11-15: The component RFConditionNode currently types props as
data: any, losing type safety; define a specific interface (e.g.,
RFConditionNodeData) that describes condition: string, color?: string | null,
scopes?: string[] and use it in the component signature (RFConditionNode({ data
}: { data: RFConditionNodeData }) or a typed prop) and update usages of
condition, color, scopes, accent to rely on that interface instead of
casting—this will ensure compile-time checks and safer refactors for
RFConditionNode and any callers that construct its data.

In `@ui/app/workspace/routing-rules/tree/views/node/rfEdgeHandle.tsx`:
- Line 7: The exported RF_HANDLE_SIZE_PX constant is not used in the component
styles; replace the hardcoded "14px" values inside the RFEdgeHandle component
(or related style block in rfEdgeHandle.tsx) with a single source of truth by
either setting a CSS custom property (e.g. --rf-handle-size) to
`${RF_HANDLE_SIZE_PX}px` on the element or by applying an inline style that uses
RF_HANDLE_SIZE_PX to compute width/height/border-radius, so sizing stays in sync
with the exported constant.

In `@ui/app/workspace/routing-rules/tree/views/node/rfRuleNode.tsx`:
- Around line 13-18: The RFRuleNode component currently types its props as any
which loses type safety; define a specific interface (e.g., RFRuleNodeData or
RFRuleNodeProps) describing the shape returned in data — include fields like
rule: RoutingRule, scopeColor: string, and any other used members (e.g.,
rule.scope as ScopeKey and rule.targets: array) — then change the component
signature to use that interface (export function RFRuleNode({ data }:
RFRuleNodeProps) or similar) so usages of SCOPE_CONFIG, RoutingRule, ScopeKey,
data.rule, data.scopeColor and rule.targets are type-checked.
- Around line 80-133: The popover anchored in the hovered block (when hovered is
true) uses fixed classes left-full and ml-3 which can overflow the viewport;
update the component to detect available space and flip or clamp the popover
instead of always using left-full: add a small local state (e.g., popoverSide or
popoverOffset) and compute placement in an effect using the node element's
getBoundingClientRect (or swap in a positioning library like Popper.js), then
conditionally apply classes (replace left-full ml-3 with right/full or a
max-width & translate-x clamp) while still using rule, scopeColor and
RenderProviderIcon as is so the visual content remains identical.

In `@ui/app/workspace/routing-rules/tree/views/routingTreeView.tsx`:
- Around line 61-62: Replace the any-typed ref by importing and using the proper
ReactFlowInstance type from '@xyflow/react' and remove the eslint-disable;
specifically change the rfInstanceRef declaration (currently const rfInstanceRef
= useRef<any>(null)) to useRef<ReactFlowInstance | null>(null) and add the
corresponding import for ReactFlowInstance so the ref is strongly typed
throughout the component.
- Around line 399-405: The nested ternary in the JSX reduces readability;
extract the logic that builds the display string into a small helper/variable
(e.g., computeRuleCountText or ruleCountText) inside the RoutingTreeView
component and return a single string to render. Implement the helper to
accept/close over search, highlightedIds, matchCount and rules.length and
produce the three possible outputs ("no match", "{matchCount} rule(s)", or
"{rules.length} rule(s)") with proper pluralization, then replace the nested
ternary in the <p> with that single variable or function call.
- Around line 180-186: The code in routingTreeView.tsx currently treats
chain-back edges by checking e.id.endsWith("-chain"), which couples this view to
the edge ID format; update the graph construction to set a canonical flag (e.g.,
add data.isChainBack = true in the rfEdges mapping inside graphBuilder.ts), then
change the exclusion logic in routingTreeView.tsx to check that flag (e.g., if
(!e.data?.isChainBack) { ... }) when populating parentsOf/childrenOf; update any
references to e.id-based logic (the current e.id.endsWith("-chain") check) to
use e.data.isChainBack so identification is explicit and decoupled from naming.
- Around line 147-155: The setTimeout used in handleResetLayout to call
rfInstanceRef.current?.fitView is fragile; replace it with a reliable
post-render callback such as requestAnimationFrame (or ReactDOM.flushSync +
immediate call) so fitView runs after the state updates complete. Update
handleResetLayout to clear cookies and update cookieDataRef/current state
(cookieDataRef, setNodes, setSelectedNodeId, setSelectedEdgeId) as before, then
call rfInstanceRef.current?.fitView from inside requestAnimationFrame (or from
an effect that watches baseNodes/nodes) to ensure layout fitting happens after
React has flushed the DOM; reference handleResetLayout, rfInstanceRef,
cookieDataRef, setNodes, setSelectedNodeId, and setSelectedEdgeId when locating
the change.

In `@ui/components/ui/asyncMultiselect.tsx`:
- Line 459: The hard-coded minMenuHeight=160 in AsyncMultiSelect should be made
configurable: add an optional prop (e.g., minMenuHeight?: number) to the
AsyncMultiSelect props/interface and replace the literal 160 with the prop value
(use the prop when provided, otherwise fall back to the component's existing
default behavior or undefined), then update the routing-rules tree callsite to
pass minMenuHeight={160} where the tighter menu is required; reference
AsyncMultiSelect and the minMenuHeight prop in your changes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 22c92334-2509-4510-8856-7744c77d1200

📥 Commits

Reviewing files that changed from the base of the PR and between c881ec9 and e6396bd.

⛔ Files ignored due to path filters (1)
  • ui/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (13)
  • framework/configstore/tables/routing_rules.go
  • ui/app/workspace/routing-rules/tree/views/celParser.ts
  • ui/app/workspace/routing-rules/tree/views/constants.ts
  • ui/app/workspace/routing-rules/tree/views/graphBuilder.ts
  • ui/app/workspace/routing-rules/tree/views/node/rfConditionNode.tsx
  • ui/app/workspace/routing-rules/tree/views/node/rfEdgeHandle.tsx
  • ui/app/workspace/routing-rules/tree/views/node/rfRuleNode.tsx
  • ui/app/workspace/routing-rules/tree/views/node/rfSourceNode.tsx
  • ui/app/workspace/routing-rules/tree/views/positionPersistence.ts
  • ui/app/workspace/routing-rules/tree/views/rfChainEdge.tsx
  • ui/app/workspace/routing-rules/tree/views/routingTreeView.tsx
  • ui/components/ui/asyncMultiselect.tsx
  • ui/package.json

@roroghost17 roroghost17 force-pushed the 03-30-feat_fixes_routing_rules_tree_view_with_better_layout_and_node_ui branch from e6396bd to 242acaa Compare March 30, 2026 23:04
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (6)
ui/app/workspace/routing-rules/tree/views/graphBuilder.ts (2)

400-401: Minor: Redundant chain-back filter.

computeLRLayout already filters out chain-back edges internally (line 332), so the filter on line 401 is redundant. Not harmful, but could be simplified.

Remove redundant filter
-	const positions = computeLRLayout(lNodes, lEdges.filter((e) => !e.isChainBack));
+	const positions = computeLRLayout(lNodes, lEdges);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/app/workspace/routing-rules/tree/views/graphBuilder.ts` around lines 400 -
401, The call to computeLRLayout currently passes lEdges.filter((e) =>
!e.isChainBack) even though computeLRLayout already excludes chain-back edges
internally; remove the redundant filter and pass lEdges directly (keep using
lNodes and lEdges as the inputs), i.e., change the call that computes positions
to call computeLRLayout(lNodes, lEdges) so the internal filtering in
computeLRLayout handles chain-back edges.

293-300: Consider combining the fallback and iteration logic.

When entries.length === 0, you add an edge to source (line 296) and then iterate over entries (line 298) which will be a no-op. The logic is correct but could be clearer with an else clause.

Clarify control flow
 				const entries = findEntries(childrenOf.get("source") ?? [], vars);
 				if (entries.length === 0) {
 					// resolved vars match no condition node — fall back to source
 					addEdge(ruleId, "source", "↺", sc, { isChainBack: true, isChainWeak: false, sourceHandle: "chain-out" });
+				} else {
+					for (const { id: condId, strong } of entries) {
+						addEdge(ruleId, condId, "↺", sc, { isChainBack: true, isChainWeak: !strong, sourceHandle: "chain-out" });
+					}
 				}
-				for (const { id: condId, strong } of entries) {
-					addEdge(ruleId, condId, "↺", sc, { isChainBack: true, isChainWeak: !strong, sourceHandle: "chain-out" });
-				}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/app/workspace/routing-rules/tree/views/graphBuilder.ts` around lines 293 -
300, The current code calls findEntries(childrenOf.get("source") ?? [], vars)
into entries, then unconditionally checks if entries.length === 0 to
addEdge(ruleId, "source", ...) and afterwards loops for (const { id: condId,
strong } of entries) which is a no-op when empty; change this to an if/else so
the fallback addEdge executes only when entries is empty and the for-loop
executes only when entries has items—update the block that references
findEntries, entries, addEdge, childrenOf, vars, ruleId, and sc to use an if
(entries.length === 0) { addEdge(...) } else { for (...) { addEdge(...) } }
structure to make control flow explicit.
ui/app/workspace/routing-rules/tree/views/routingTreeView.tsx (3)

61-64: Consider typing the React Flow instance ref.

The any type suppression works but React Flow exports ReactFlowInstance type that could be used for better type safety.

Type the ref
+import type { ReactFlowInstance } from "@xyflow/react";
 // ...
-	// eslint-disable-next-line `@typescript-eslint/no-explicit-any`
-	const rfInstanceRef = useRef<any>(null);
+	const rfInstanceRef = useRef<ReactFlowInstance | null>(null);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/app/workspace/routing-rules/tree/views/routingTreeView.tsx` around lines
61 - 64, The rfInstanceRef is typed as any; replace useRef<any>(null) with
useRef<ReactFlowInstance | null>(null) and import the ReactFlowInstance type
from the reactflow package (e.g., import type { ReactFlowInstance } from
'reactflow'), remove the eslint-disable comment, and update any usages of
rfInstanceRef.current to handle the nullable type (guard or optional chaining)
so the code remains type-safe.

411-428: Consider adding data-testid attributes to interactive toolbar elements.

The search input and reset layout button lack data-testid attributes. Per coding guidelines, interactive UI elements should have testids for e2e testing.

Add testids
 						<Input
 							value={search}
 							onChange={(e) => setSearch(e.target.value)}
 							placeholder="Search conditions or rules…"
 							className="h-8 w-56 pl-8 text-sm"
+							data-testid="routing-tree-search-input"
 						/>
 					</div>
 					<div className="h-5 w-px bg-border" />
 					<Button
 						variant="ghost" size="sm" className="gap-1.5 text-muted-foreground hover:text-foreground"
 						onClick={handleResetLayout}
 						title="Reset to default layout"
+						data-testid="routing-tree-reset-btn"
 					>

As per coding guidelines: ui/**/*.{tsx,ts}: Add new interactive UI elements with data-testid attributes following the pattern: data-testid="<entity>-<element>-<qualifier>".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/app/workspace/routing-rules/tree/views/routingTreeView.tsx` around lines
411 - 428, Add data-testid attributes to the interactive toolbar elements: add
data-testid="routing-tree-search-input" to the Input used for search (the Input
component bound to search with onChange) and add
data-testid="routing-tree-reset-button" to the Button that calls
handleResetLayout (the Button containing RotateCcw and "Reset layout"). Ensure
attributes follow the pattern entity-element-qualifier and are added on the
Input and Button elements in routingTreeView.tsx.

184-188: Chain-back edge detection relies on ID format convention.

The detection e.id.endsWith("-chain") depends on the edge ID format in graphBuilder.ts (line 419: e-${le.source}-${le.target}${le.isChainBack ? "-chain" : ""}). This coupling is implicit.

Consider documenting this convention or using the edge data to detect chain-back edges instead (the data includes chainWeak for chain edges).

Use edge type instead of ID suffix
 		for (const e of edges) {
 			if (!childrenOf.has(e.source)) childrenOf.set(e.source, []);
 			childrenOf.get(e.source)!.push(e.target);
 			// Exclude chain-back edges from the ancestor map (they reverse flow direction).
-			if (!e.id.endsWith("-chain")) {
+			if (e.type !== "rfChain") {
 				if (!parentsOf.has(e.target)) parentsOf.set(e.target, []);
 				parentsOf.get(e.target)!.push(e.source);
 			}
 		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/app/workspace/routing-rules/tree/views/routingTreeView.tsx` around lines
184 - 188, The current chain-back detection uses the edge id suffix check
(e.id.endsWith("-chain")) which couples behavior to the ID format; instead,
update the conditional to use the edge metadata (e.data?.chainWeak or the
explicit chain flag available on the edge object) so chain-back edges are
detected from their data rather than the id string: replace
e.id.endsWith("-chain") with a check like e.data?.chainWeak === true (or the
appropriate boolean flag on the edge) in the block that populates parentsOf
(refer to the parentsOf map and the edge variable e), or alternately add a short
comment documenting the ID convention if you intentionally keep the suffix
approach.
ui/app/workspace/routing-rules/tree/views/node/rfRuleNode.tsx (1)

13-18: Consider adding data-testid for the interactive rule node.

This component renders an interactive, draggable node that users can hover and click. Per coding guidelines, interactive UI elements should have data-testid attributes following the pattern <entity>-<element>-<qualifier>.

Suggested testid addition
 export function RFRuleNode({ data }: { data: any }) {
 	const rule = data.rule as RoutingRule;
 	const scopeColor = data.scopeColor as string;
 	const cfg = SCOPE_CONFIG[rule.scope as ScopeKey];
-	const multi = rule.targets.length > 1;
+	const multi = rule.targets?.length > 1;
 	const [hovered, setHovered] = useState(false);

And on the outer div:

 	return (
 		<div
 			className="relative"
 			style={{ width: RULE_W }}
 			onMouseEnter={() => setHovered(true)}
 			onMouseLeave={() => setHovered(false)}
+			data-testid={`routing-rule-node-${rule.id}`}
 		>

As per coding guidelines: ui/**/*.{tsx,ts}: Add new interactive UI elements with data-testid attributes following the pattern: data-testid="<entity>-<element>-<qualifier>".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/app/workspace/routing-rules/tree/views/node/rfRuleNode.tsx` around lines
13 - 18, Add a data-testid to the interactive outer element in RFRuleNode so
tests can target it; specifically, on the outer div (the element that renders
the draggable/hoverable node inside the RFRuleNode component) add a data-testid
following the pattern "<entity>-<element>-<qualifier>" (e.g.,
"routing-rule-node-interactive" or use a unique instance-specific id like
"routing-rule-node-<rule.id>") to make the element queryable in tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@ui/components/ui/asyncMultiselect.tsx`:
- Line 459: The change reduces the AsyncMultiSelect prop minMenuHeight to 160
which globally affects all consumers (virtualKeySheet.tsx, valueEditor.tsx,
mcpServerSelector.tsx, mcpToolSelector.tsx, modelMultiselect.tsx,
entityAssociationSelect.tsx); confirm UX for long lists and either revert to the
previous default or make minMenuHeight configurable per consumer by adding a
prop (e.g., minMenuHeight) to the AsyncMultiSelect component and thread it
through where used so each consumer (notably mcpServerSelector.tsx,
mcpToolSelector.tsx, virtualKeySheet.tsx) can override the value to meet their
layout/scrolling needs. Ensure the AsyncMultiSelect default remains documented
and tests or screenshots from those six consumers are updated to validate the
visual impact.

---

Nitpick comments:
In `@ui/app/workspace/routing-rules/tree/views/graphBuilder.ts`:
- Around line 400-401: The call to computeLRLayout currently passes
lEdges.filter((e) => !e.isChainBack) even though computeLRLayout already
excludes chain-back edges internally; remove the redundant filter and pass
lEdges directly (keep using lNodes and lEdges as the inputs), i.e., change the
call that computes positions to call computeLRLayout(lNodes, lEdges) so the
internal filtering in computeLRLayout handles chain-back edges.
- Around line 293-300: The current code calls
findEntries(childrenOf.get("source") ?? [], vars) into entries, then
unconditionally checks if entries.length === 0 to addEdge(ruleId, "source", ...)
and afterwards loops for (const { id: condId, strong } of entries) which is a
no-op when empty; change this to an if/else so the fallback addEdge executes
only when entries is empty and the for-loop executes only when entries has
items—update the block that references findEntries, entries, addEdge,
childrenOf, vars, ruleId, and sc to use an if (entries.length === 0) {
addEdge(...) } else { for (...) { addEdge(...) } } structure to make control
flow explicit.

In `@ui/app/workspace/routing-rules/tree/views/node/rfRuleNode.tsx`:
- Around line 13-18: Add a data-testid to the interactive outer element in
RFRuleNode so tests can target it; specifically, on the outer div (the element
that renders the draggable/hoverable node inside the RFRuleNode component) add a
data-testid following the pattern "<entity>-<element>-<qualifier>" (e.g.,
"routing-rule-node-interactive" or use a unique instance-specific id like
"routing-rule-node-<rule.id>") to make the element queryable in tests.

In `@ui/app/workspace/routing-rules/tree/views/routingTreeView.tsx`:
- Around line 61-64: The rfInstanceRef is typed as any; replace
useRef<any>(null) with useRef<ReactFlowInstance | null>(null) and import the
ReactFlowInstance type from the reactflow package (e.g., import type {
ReactFlowInstance } from 'reactflow'), remove the eslint-disable comment, and
update any usages of rfInstanceRef.current to handle the nullable type (guard or
optional chaining) so the code remains type-safe.
- Around line 411-428: Add data-testid attributes to the interactive toolbar
elements: add data-testid="routing-tree-search-input" to the Input used for
search (the Input component bound to search with onChange) and add
data-testid="routing-tree-reset-button" to the Button that calls
handleResetLayout (the Button containing RotateCcw and "Reset layout"). Ensure
attributes follow the pattern entity-element-qualifier and are added on the
Input and Button elements in routingTreeView.tsx.
- Around line 184-188: The current chain-back detection uses the edge id suffix
check (e.id.endsWith("-chain")) which couples behavior to the ID format;
instead, update the conditional to use the edge metadata (e.data?.chainWeak or
the explicit chain flag available on the edge object) so chain-back edges are
detected from their data rather than the id string: replace
e.id.endsWith("-chain") with a check like e.data?.chainWeak === true (or the
appropriate boolean flag on the edge) in the block that populates parentsOf
(refer to the parentsOf map and the edge variable e), or alternately add a short
comment documenting the ID convention if you intentionally keep the suffix
approach.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4e749f71-1664-430e-8372-28b24e384a1d

📥 Commits

Reviewing files that changed from the base of the PR and between e6396bd and 242acaa.

⛔ Files ignored due to path filters (1)
  • ui/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (13)
  • framework/configstore/tables/routing_rules.go
  • ui/app/workspace/routing-rules/tree/views/celParser.ts
  • ui/app/workspace/routing-rules/tree/views/constants.ts
  • ui/app/workspace/routing-rules/tree/views/graphBuilder.ts
  • ui/app/workspace/routing-rules/tree/views/node/rfConditionNode.tsx
  • ui/app/workspace/routing-rules/tree/views/node/rfEdgeHandle.tsx
  • ui/app/workspace/routing-rules/tree/views/node/rfRuleNode.tsx
  • ui/app/workspace/routing-rules/tree/views/node/rfSourceNode.tsx
  • ui/app/workspace/routing-rules/tree/views/positionPersistence.ts
  • ui/app/workspace/routing-rules/tree/views/rfChainEdge.tsx
  • ui/app/workspace/routing-rules/tree/views/routingTreeView.tsx
  • ui/components/ui/asyncMultiselect.tsx
  • ui/package.json
✅ Files skipped from review due to trivial changes (6)
  • ui/package.json
  • framework/configstore/tables/routing_rules.go
  • ui/app/workspace/routing-rules/tree/views/positionPersistence.ts
  • ui/app/workspace/routing-rules/tree/views/node/rfSourceNode.tsx
  • ui/app/workspace/routing-rules/tree/views/node/rfEdgeHandle.tsx
  • ui/app/workspace/routing-rules/tree/views/constants.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • ui/app/workspace/routing-rules/tree/views/node/rfConditionNode.tsx
  • ui/app/workspace/routing-rules/tree/views/rfChainEdge.tsx
  • ui/app/workspace/routing-rules/tree/views/celParser.ts

Comment thread ui/components/ui/asyncMultiselect.tsx
@roroghost17 roroghost17 force-pushed the 03-30-feat_fixes_routing_rules_tree_view_with_better_layout_and_node_ui branch 2 times, most recently from 2f96f62 to bff3dc0 Compare March 31, 2026 07:33
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@ui/app/workspace/routing-rules/tree/views/celParser.ts`:
- Around line 79-115: The current expandCEL only fans out top-level ||, so
expressions like a && (b || c) remain as a single parenthesized part; update
expandCEL (and if helpful, splitOn/isWrappedInParens) to recursively distribute
ORs inside parenthesized groups: when splitting on "||" detect branches that are
parenthesized and themselves contain "||" (use isWrappedInParens to detect) then
call expandCEL on the inner content to produce multiple branch variants and
combine them with the surrounding AND parts (Cartesian product) so a && (b || c)
expands to ["a","b"] and ["a","c"]; add a regression unit test for the grouped
disjunction case (e.g., "a && (b || c)") to assert the fan-out occurs.

In `@ui/app/workspace/routing-rules/tree/views/graphBuilder.ts`:
- Around line 140-163: collectTerminals() can return the same RoutingRule
multiple times (one per OR branch), which makes nodeColor() overweight scopes
for rules that expand into multiple terminals; change nodeColor() to deduplicate
rules by rule.id after calling collectTerminals() (e.g., build a Set of rule.id
or a Map from id→rule) and then compute counts only from the unique rules before
creating the counts Map, passing those deduped counts into blendColors and
caching the result; keep references to collectTerminals, nodeColor,
RoutingRule.rule.id, counts, SCOPE_CONFIG and blendColors to locate the changes.
- Around line 240-299: findEntries currently drops branches that evaluated to
null, causing the caller to treat unresolved fallbacks as static loops; update
findEntries (and its inner explore) to propagate "unknown" by pushing the
deepest condition node when a descendant returned null (emit the condition id
with strong:false), or return an explicit flag signaling unresolved; then update
the caller loop that consumes findEntries (the block that calls entries =
findEntries(...) and the fallback branch that calls addEdge(..., { isChainBack:
true, isChainWeak: false, ... })) to treat those propagated unresolved entries
as weak (set isChainWeak: true) instead of marking the chain as static.

In `@ui/app/workspace/routing-rules/tree/views/node/rfRuleNode.tsx`:
- Around line 21-26: The rule detail popover is only shown on mouse hover (the
div with onMouseEnter/onMouseLeave that sets setHovered and the similar handlers
in the 81-133 range), making it inaccessible to keyboard and touch users; update
these nodes to also open the same detail view on keyboard focus and activation
by adding onFocus/onBlur and keyboard handlers (onKeyDown for Enter/Space) and a
click toggle that calls the same setHovered/setDetailVisible state, and ensure
the rendered popover includes proper ARIA attributes (aria-haspopup,
aria-expanded) and focus management so it can be opened/closed via keyboard and
is reachable on touch devices.

In `@ui/app/workspace/routing-rules/tree/views/routingTreeView.tsx`:
- Around line 421-428: The new interactive controls in routingTreeView need
stable test selectors: add data-testid="routing-tree-reset-layout-btn" to the
Reset layout Button (the Button with onClick={handleResetLayout} and child
<RotateCcw />), and add data-testid="routing-tree-static-chain-info-trigger" and
data-testid="routing-tree-dynamic-chain-info-trigger" to the two legend info
trigger elements referenced around the legend rendering (the interactive
elements that open static/dynamic chain info); ensure attributes are added
directly on those interactive JSX elements so tests can target them.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3c0c9129-6882-4c98-8c24-bddc6e481854

📥 Commits

Reviewing files that changed from the base of the PR and between 242acaa and 2f96f62.

⛔ Files ignored due to path filters (1)
  • ui/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (14)
  • framework/configstore/tables/routing_rules.go
  • ui/app/globals.css
  • ui/app/workspace/routing-rules/tree/views/celParser.ts
  • ui/app/workspace/routing-rules/tree/views/constants.ts
  • ui/app/workspace/routing-rules/tree/views/graphBuilder.ts
  • ui/app/workspace/routing-rules/tree/views/node/rfConditionNode.tsx
  • ui/app/workspace/routing-rules/tree/views/node/rfEdgeHandle.tsx
  • ui/app/workspace/routing-rules/tree/views/node/rfRuleNode.tsx
  • ui/app/workspace/routing-rules/tree/views/node/rfSourceNode.tsx
  • ui/app/workspace/routing-rules/tree/views/positionPersistence.ts
  • ui/app/workspace/routing-rules/tree/views/rfChainEdge.tsx
  • ui/app/workspace/routing-rules/tree/views/routingTreeView.tsx
  • ui/components/ui/asyncMultiselect.tsx
  • ui/package.json
✅ Files skipped from review due to trivial changes (4)
  • ui/package.json
  • ui/app/globals.css
  • ui/app/workspace/routing-rules/tree/views/positionPersistence.ts
  • ui/app/workspace/routing-rules/tree/views/constants.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • framework/configstore/tables/routing_rules.go
  • ui/app/workspace/routing-rules/tree/views/node/rfSourceNode.tsx
  • ui/app/workspace/routing-rules/tree/views/node/rfEdgeHandle.tsx
  • ui/app/workspace/routing-rules/tree/views/node/rfConditionNode.tsx

Comment thread ui/app/workspace/routing-rules/tree/views/celParser.ts
Comment thread ui/app/workspace/routing-rules/tree/views/graphBuilder.ts
Comment thread ui/app/workspace/routing-rules/tree/views/graphBuilder.ts
Comment thread ui/app/workspace/routing-rules/tree/views/node/rfRuleNode.tsx
Comment thread ui/app/workspace/routing-rules/tree/views/routingTreeView.tsx
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
ui/app/workspace/routing-rules/tree/views/graphBuilder.ts (1)

148-164: ⚠️ Potential issue | 🟡 Minor

Deduplicate terminal rules before blending scope colors.

collectTerminals() returns the same RoutingRule once per OR-expanded terminal path, so nodeColor() weights the blend by that raw array. A rule with more OR branches will skew the condition color toward its scope even though it's still only one rule. Consider counting unique rule.ids before building counts.

🔧 Suggested fix
 function nodeColor(node: TrieNode, cache?: Map<string, string | null>): string | null {
 	if (cache?.has(node.id)) return cache.get(node.id)!;
 	const rules = collectTerminals(node);
 	if (!rules.length) { cache?.set(node.id, null); return null; }
+	// Deduplicate by rule ID to avoid OR-branch skew
+	const uniqueRules = [...new Map(rules.map((r) => [r.id, r])).values()];
 	// Count rules per scope to produce a weighted blend.
 	const counts = new Map<string, number>();
-	for (const r of rules) counts.set(r.scope, (counts.get(r.scope) ?? 0) + 1);
+	for (const r of uniqueRules) counts.set(r.scope, (counts.get(r.scope) ?? 0) + 1);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/app/workspace/routing-rules/tree/views/graphBuilder.ts` around lines 148 -
164, nodeColor currently counts each RoutingRule returned by collectTerminals
including duplicates from OR-expanded paths, which skews blending; modify
nodeColor (around the collectTerminals usage) to deduplicate rules by their
unique rule.id before computing counts—e.g., build a Set or Map of rule.id to
ensure each rule contributes only once, then iterate those unique rules to
populate the counts Map and proceed with the existing color blending and cache
behavior.
🧹 Nitpick comments (5)
ui/app/workspace/routing-rules/tree/views/node/rfRuleNode.tsx (2)

13-18: Consider adding a typed interface for the data prop.

Same as RFConditionNode, using data: any loses type safety. The expected shape is { rule: RoutingRule; scopeColor: string }.

♻️ Suggested type definition
+interface RFRuleNodeData {
+	rule: RoutingRule;
+	scopeColor: string;
+}
+
-export function RFRuleNode({ data }: { data: any }) {
-	const rule = data.rule as RoutingRule;
-	const scopeColor = data.scopeColor as string;
+export function RFRuleNode({ data }: { data: RFRuleNodeData }) {
+	const { rule, scopeColor } = data;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/app/workspace/routing-rules/tree/views/node/rfRuleNode.tsx` around lines
13 - 18, The component RFRuleNode currently types its prop as any; define a
specific props interface (e.g., RFRuleNodeProps) that matches the expected shape
(at minimum rule: RoutingRule and scopeColor: string — include targets or other
fields used by the component if present) and update the function signature to
use RFRuleNodeProps instead of any; reference RFConditionNode as an example for
the pattern and ensure imports/types for RoutingRule and ScopeKey remain used
with SCOPE_CONFIG where needed.

81-133: Popover may overflow the viewport on rightmost nodes.

The popover is absolutely positioned to the right of the node (left-full ml-3). For rule nodes near the right edge of the canvas, this can push the popover outside the visible area. Consider detecting available space or using a library like Radix Popover with collision detection.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/app/workspace/routing-rules/tree/views/node/rfRuleNode.tsx` around lines
81 - 133, The popover rendered inside the hovered conditional (the JSX block
that returns the div with class "absolute left-full ... ml-3" in rfRuleNode.tsx)
can overflow on right-edge nodes; update this to use collision-aware placement
(e.g., replace the manual absolutely positioned div with a Radix Popover or
similar that supports collision detection and automatic flipping), or implement
runtime placement logic: measure available space from the node (when hovered is
true) and switch the popover container's placement class from "left-full ml-3"
to a left-side anchor (e.g., right-full mr-3) when there isn’t enough room;
ensure the change is applied where hovered is used and the popover wrapper div
(the element currently using scopeColor for border) is replaced so targets
mapping and styles remain unchanged.
ui/app/workspace/routing-rules/tree/views/routingTreeView.tsx (1)

60-64: Consider typing the React Flow instance ref.

The any type with eslint-disable is understandable given React Flow's complex instance type, but you could use ReactFlowInstance from @xyflow/react for better type safety.

♻️ Suggested fix
+import type { ReactFlowInstance } from "@xyflow/react";
+
 // React Flow instance — captured via onInit so we can call fitView imperatively.
-// eslint-disable-next-line `@typescript-eslint/no-explicit-any`
-const rfInstanceRef = useRef<any>(null);
+const rfInstanceRef = useRef<ReactFlowInstance | null>(null);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/app/workspace/routing-rules/tree/views/routingTreeView.tsx` around lines
60 - 64, The rfInstanceRef currently uses an any type with an eslint-disable;
change it to a properly typed React Flow instance by importing ReactFlowInstance
from '@xyflow/react' and declaring rfInstanceRef as useRef<ReactFlowInstance |
null>(null) (and remove the eslint-disable comment). Update references to
rfInstanceRef elsewhere if needed to account for the nullable type (e.g., check
for null before calling methods like fitView).
ui/app/workspace/routing-rules/tree/views/graphBuilder.ts (1)

360-365: Consider extracting the source offset as a named constant.

The hardcoded 200 magic number for the source node's horizontal offset reduces readability. Consider adding it to constants.ts alongside other layout values.

♻️ Suggested refactor

In constants.ts:

export const SOURCE_LEFT_OFFSET = 200;

In graphBuilder.ts:

+import { SOURCE_LEFT_OFFSET, ... } from "./constants";
 // ...
 const sourcePos = positions.get("source");
-if (sourcePos) sourcePos.x -= 200;
+if (sourcePos) sourcePos.x -= SOURCE_LEFT_OFFSET;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/app/workspace/routing-rules/tree/views/graphBuilder.ts` around lines 360 -
365, Replace the magic number used to offset the source node with a named
constant: add SOURCE_LEFT_OFFSET to constants.ts and use it in graphBuilder.ts
when adjusting the source position (the code around positions.get("source") and
sourcePos.x -= 200); import SOURCE_LEFT_OFFSET and apply sourcePos.x -=
SOURCE_LEFT_OFFSET to improve readability and centralize layout configuration.
ui/app/workspace/routing-rules/tree/views/node/rfConditionNode.tsx (1)

11-14: Consider adding a typed interface for the data prop.

Using data: any sacrifices type safety. A minimal interface would help catch mismatches between graphBuilder.ts and this component.

♻️ Suggested type definition
+interface RFConditionNodeData {
+	condition: string;
+	color?: string | null;
+	scopes?: string[];
+}
+
-export function RFConditionNode({ data }: { data: any }) {
-	const condition = data.condition as string;
-	const color = data.color as string | null;
-	const scopes = (data.scopes as string[] | undefined) ?? [];
+export function RFConditionNode({ data }: { data: RFConditionNodeData }) {
+	const condition = data.condition;
+	const color = data.color ?? null;
+	const scopes = data.scopes ?? [];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/app/workspace/routing-rules/tree/views/node/rfConditionNode.tsx` around
lines 11 - 14, The component RFConditionNode currently types its prop as data:
any; introduce a clear interface (e.g., RFConditionData or RFConditionNodeProps)
that declares condition: string, color: string | null, and scopes?: string[] and
use it in the function signature instead of any; update the destructuring to use
that typed prop and export the interface so graphBuilder.ts can import/reuse it
to keep both places in sync and catch mismatches early.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@ui/app/workspace/routing-rules/tree/views/graphBuilder.ts`:
- Around line 148-164: nodeColor currently counts each RoutingRule returned by
collectTerminals including duplicates from OR-expanded paths, which skews
blending; modify nodeColor (around the collectTerminals usage) to deduplicate
rules by their unique rule.id before computing counts—e.g., build a Set or Map
of rule.id to ensure each rule contributes only once, then iterate those unique
rules to populate the counts Map and proceed with the existing color blending
and cache behavior.

---

Nitpick comments:
In `@ui/app/workspace/routing-rules/tree/views/graphBuilder.ts`:
- Around line 360-365: Replace the magic number used to offset the source node
with a named constant: add SOURCE_LEFT_OFFSET to constants.ts and use it in
graphBuilder.ts when adjusting the source position (the code around
positions.get("source") and sourcePos.x -= 200); import SOURCE_LEFT_OFFSET and
apply sourcePos.x -= SOURCE_LEFT_OFFSET to improve readability and centralize
layout configuration.

In `@ui/app/workspace/routing-rules/tree/views/node/rfConditionNode.tsx`:
- Around line 11-14: The component RFConditionNode currently types its prop as
data: any; introduce a clear interface (e.g., RFConditionData or
RFConditionNodeProps) that declares condition: string, color: string | null, and
scopes?: string[] and use it in the function signature instead of any; update
the destructuring to use that typed prop and export the interface so
graphBuilder.ts can import/reuse it to keep both places in sync and catch
mismatches early.

In `@ui/app/workspace/routing-rules/tree/views/node/rfRuleNode.tsx`:
- Around line 13-18: The component RFRuleNode currently types its prop as any;
define a specific props interface (e.g., RFRuleNodeProps) that matches the
expected shape (at minimum rule: RoutingRule and scopeColor: string — include
targets or other fields used by the component if present) and update the
function signature to use RFRuleNodeProps instead of any; reference
RFConditionNode as an example for the pattern and ensure imports/types for
RoutingRule and ScopeKey remain used with SCOPE_CONFIG where needed.
- Around line 81-133: The popover rendered inside the hovered conditional (the
JSX block that returns the div with class "absolute left-full ... ml-3" in
rfRuleNode.tsx) can overflow on right-edge nodes; update this to use
collision-aware placement (e.g., replace the manual absolutely positioned div
with a Radix Popover or similar that supports collision detection and automatic
flipping), or implement runtime placement logic: measure available space from
the node (when hovered is true) and switch the popover container's placement
class from "left-full ml-3" to a left-side anchor (e.g., right-full mr-3) when
there isn’t enough room; ensure the change is applied where hovered is used and
the popover wrapper div (the element currently using scopeColor for border) is
replaced so targets mapping and styles remain unchanged.

In `@ui/app/workspace/routing-rules/tree/views/routingTreeView.tsx`:
- Around line 60-64: The rfInstanceRef currently uses an any type with an
eslint-disable; change it to a properly typed React Flow instance by importing
ReactFlowInstance from '@xyflow/react' and declaring rfInstanceRef as
useRef<ReactFlowInstance | null>(null) (and remove the eslint-disable comment).
Update references to rfInstanceRef elsewhere if needed to account for the
nullable type (e.g., check for null before calling methods like fitView).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: bdb11502-8c46-4214-a1e7-6143ee1a874d

📥 Commits

Reviewing files that changed from the base of the PR and between 2f96f62 and bff3dc0.

⛔ Files ignored due to path filters (1)
  • ui/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (14)
  • framework/configstore/tables/routing_rules.go
  • ui/app/globals.css
  • ui/app/workspace/routing-rules/tree/views/celParser.ts
  • ui/app/workspace/routing-rules/tree/views/constants.ts
  • ui/app/workspace/routing-rules/tree/views/graphBuilder.ts
  • ui/app/workspace/routing-rules/tree/views/node/rfConditionNode.tsx
  • ui/app/workspace/routing-rules/tree/views/node/rfEdgeHandle.tsx
  • ui/app/workspace/routing-rules/tree/views/node/rfRuleNode.tsx
  • ui/app/workspace/routing-rules/tree/views/node/rfSourceNode.tsx
  • ui/app/workspace/routing-rules/tree/views/positionPersistence.ts
  • ui/app/workspace/routing-rules/tree/views/rfChainEdge.tsx
  • ui/app/workspace/routing-rules/tree/views/routingTreeView.tsx
  • ui/components/ui/asyncMultiselect.tsx
  • ui/package.json
✅ Files skipped from review due to trivial changes (5)
  • ui/package.json
  • ui/app/workspace/routing-rules/tree/views/positionPersistence.ts
  • ui/app/workspace/routing-rules/tree/views/node/rfEdgeHandle.tsx
  • ui/app/workspace/routing-rules/tree/views/constants.ts
  • ui/app/workspace/routing-rules/tree/views/celParser.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • framework/configstore/tables/routing_rules.go
  • ui/app/workspace/routing-rules/tree/views/node/rfSourceNode.tsx
  • ui/app/globals.css

@Pratham-Mishra04 Pratham-Mishra04 changed the base branch from 03-26-feat_adds_routing_tree_view to graphite-base/2407 April 1, 2026 05:28
@roroghost17 roroghost17 force-pushed the 03-30-feat_fixes_routing_rules_tree_view_with_better_layout_and_node_ui branch from bff3dc0 to 480d3b0 Compare April 1, 2026 06:04
coderabbitai[bot]
coderabbitai Bot previously approved these changes Apr 1, 2026
@roroghost17 roroghost17 force-pushed the 03-30-feat_fixes_routing_rules_tree_view_with_better_layout_and_node_ui branch from 480d3b0 to e36f409 Compare April 1, 2026 19:12
Copy link
Copy Markdown
Contributor

akshaydeo commented Apr 2, 2026

Merge activity

  • Apr 2, 10:02 AM UTC: A user started a stack merge that includes this pull request via Graphite.
  • Apr 2, 10:05 AM UTC: Graphite couldn't merge this pull request because a downstack PR feat: add routing rules tree visualization #2297 failed to merge.
  • Apr 2, 10:57 AM UTC: A user started a stack merge that includes this pull request via Graphite.
  • Apr 2, 11:00 AM UTC: Graphite couldn't merge this PR because it had merge conflicts.
  • Apr 2, 11:05 AM UTC: A user started a stack merge that includes this pull request via Graphite.
  • Apr 2, 11:06 AM UTC: @Pratham-Mishra04 merged this pull request with Graphite.

@akshaydeo akshaydeo changed the base branch from graphite-base/2407 to v1.5.0 April 2, 2026 11:00
@akshaydeo akshaydeo dismissed Pratham-Mishra04’s stale review April 2, 2026 11:00

The base branch was changed.

@akshaydeo akshaydeo dismissed coderabbitai[bot]’s stale review April 2, 2026 11:00

The base branch was changed.

@Pratham-Mishra04 Pratham-Mishra04 force-pushed the 03-30-feat_fixes_routing_rules_tree_view_with_better_layout_and_node_ui branch from e36f409 to 22f1a40 Compare April 2, 2026 11:04
@Pratham-Mishra04 Pratham-Mishra04 merged commit 1bc01aa into v1.5.0 Apr 2, 2026
16 of 17 checks passed
@Pratham-Mishra04 Pratham-Mishra04 deleted the 03-30-feat_fixes_routing_rules_tree_view_with_better_layout_and_node_ui branch April 2, 2026 11:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants