-
-
Notifications
You must be signed in to change notification settings - Fork 2
Description
Overview
renderAgentTree in apps/mcp-server/src/tui/components/activity-visualizer.pure.ts currently renders only the root node and its direct children (depth=1). When agents form a deeper delegation chain (e.g., root → child → grandchild), the grandchild nodes are silently omitted.
This was identified as a [Low] finding during the EVAL phase of #551 and tracked here for future implementation.
Current Behavior
📊 Activity
● main-agent ← root (depth 0)
├ ● solution-arch ← direct child (depth 1) ✓ shown
└ ● researcher ← direct child (depth 1) ✓ shown
# sub-agent spawned by solution-arch is NOT shown:
# solution-arch → code-reviewer (depth 2) ✗ missing
Root Cause (lines 56–61)
// apps/mcp-server/src/tui/components/activity-visualizer.pure.ts
// Collect children: edge-based agents + activeSkills as leaf nodes
const childIds = childrenOf.get(root.id) ?? []; // ← only root's children
const agentChildren: TreeChild[] = childIds
.filter(id => agents.has(id))
.map(id => ({ type: 'agent', node: agents.get(id)! }));The childrenOf map is built for all edges, but only root.id's children are queried. Subtrees at depth ≥ 2 are never traversed.
Expected Behavior
📊 Activity
● main-agent ← depth 0
├ ● solution-architect ← depth 1
│ └ ● code-reviewer ← depth 2 ✓ shown
└ ● researcher ← depth 1
◉ brainstorming (skill) ← skill leaf
Rules:
- Depth grows until
lines.length >= height(respect height limit) - Indentation: 2 spaces per depth level (
× depth) - Connector:
├for non-last child,└for last child - Continuation:
│for ancestor is-not-last columns (standard tree drawing) - Circular edge detection: skip already-visited node IDs
- Active skills remain as leaf nodes appended after all agent children of root
Implementation Plan (TDD)
Files to modify
- Modify:
apps/mcp-server/src/tui/components/activity-visualizer.pure.ts - Modify:
apps/mcp-server/src/tui/components/activity-visualizer.pure.spec.ts
Task 1: Write failing tests for multi-level rendering
Add to activity-visualizer.pure.spec.ts:
it('shows grandchild agent (depth 2)', () => {
const agents = new Map<string, DashboardNode>();
agents.set('root', createDefaultDashboardNode({ id: 'root', name: 'root', stage: 'PLAN', status: 'running', isPrimary: true }));
agents.set('child', createDefaultDashboardNode({ id: 'child', name: 'child', stage: 'PLAN', status: 'running' }));
agents.set('grandchild', createDefaultDashboardNode({ id: 'grandchild', name: 'grandchild', stage: 'PLAN', status: 'idle' }));
const edges: Edge[] = [
{ from: 'root', to: 'child', label: '', type: 'delegation' },
{ from: 'child', to: 'grandchild', label: '', type: 'delegation' },
];
const lines = renderAgentTree(agents, edges, [], 80, 10);
expect(lines.join('\n')).toContain('grandchild');
});
it('does not enter infinite loop on cyclic edges', () => {
const agents = new Map<string, DashboardNode>();
agents.set('a', createDefaultDashboardNode({ id: 'a', name: 'a', stage: 'PLAN', status: 'running', isPrimary: true }));
agents.set('b', createDefaultDashboardNode({ id: 'b', name: 'b', stage: 'PLAN', status: 'running' }));
const edges: Edge[] = [
{ from: 'a', to: 'b', label: '', type: 'delegation' },
{ from: 'b', to: 'a', label: '', type: 'delegation' }, // cycle
];
expect(() => renderAgentTree(agents, edges, [], 80, 10)).not.toThrow();
});
it('indents grandchildren deeper than direct children', () => {
// grandchild line should have more leading spaces than child line
const agents = new Map<string, DashboardNode>();
agents.set('root', createDefaultDashboardNode({ id: 'root', name: 'root', stage: 'PLAN', status: 'running', isPrimary: true }));
agents.set('child', createDefaultDashboardNode({ id: 'child', name: 'child', stage: 'PLAN', status: 'running' }));
agents.set('grandchild', createDefaultDashboardNode({ id: 'grandchild', name: 'grandchild', stage: 'PLAN', status: 'idle' }));
const edges: Edge[] = [
{ from: 'root', to: 'child', label: '', type: 'delegation' },
{ from: 'child', to: 'grandchild', label: '', type: 'delegation' },
];
const lines = renderAgentTree(agents, edges, [], 80, 10);
const childLine = lines.find(l => l.includes('child') && !l.includes('grandchild'))!;
const grandchildLine = lines.find(l => l.includes('grandchild'))!;
const childIndent = childLine.search(/\S/);
const grandchildIndent = grandchildLine.search(/\S/);
expect(grandchildIndent).toBeGreaterThan(childIndent);
});Run to confirm RED:
cd apps/mcp-server && yarn test --run src/tui/components/activity-visualizer.pure.spec.tsTask 2: Refactor renderAgentTree to use recursive subtree traversal
Replace the flat child-collection logic with a recursive renderSubtree helper:
function renderSubtree(
nodeId: string,
agents: Map<string, DashboardNode>,
childrenOf: Map<string, string[]>,
visited: Set<string>,
depth: number,
prefix: string, // e.g. ' │ ' for ancestor columns
isLast: boolean,
lines: string[],
height: number,
width: number,
): void {
if (lines.length >= height) return;
if (visited.has(nodeId)) return; // cycle guard
visited.add(nodeId);
const node = agents.get(nodeId);
if (!node) return;
const connector = isLast ? '└' : '├';
const icon = ACTIVITY_STATUS_ICONS[node.status] ?? '?';
lines.push(truncateToDisplayWidth(`${prefix}${connector} ${icon} ${node.name}`, width));
const childIds = (childrenOf.get(nodeId) ?? []).filter(id => agents.has(id));
const nextPrefix = prefix + (isLast ? ' ' : '│ ');
for (let i = 0; i < childIds.length; i++) {
if (lines.length >= height) break;
renderSubtree(childIds[i], agents, childrenOf, visited, depth + 1, nextPrefix, i === childIds.length - 1, lines, height, width);
}
}Update renderAgentTree to call renderSubtree for each direct child of root, then append skill nodes.
Task 3: Run tests to confirm GREEN
cd apps/mcp-server && yarn test --run src/tui/components/activity-visualizer.pure.spec.tsExpected: All tests pass including the 3 new ones.
Task 4: Run full test suite
cd apps/mcp-server && yarn test --runExpected: All tests pass, no regressions.
Task 5: TypeScript check
cd apps/mcp-server && yarn tsc --noEmitExpected: No errors.
Acceptance Criteria
- Agents at depth ≥ 2 are rendered when connected via
edges - Cyclic edges do not cause infinite loops (visited set guard)
- Each depth level has 2 additional leading spaces vs. parent
-
│continuation lines drawn for non-last ancestors - Height limit (
lines.length >= height) respected at all depths - Width truncation (
truncateToDisplayWidth) applied to all lines - Active skills still rendered as leaf nodes after agent subtree
- All existing 33 tests continue to pass
- 3 new tests (grandchild, cycle, indentation) pass
- TypeScript strict mode, no
any
Context
- Discovered during: EVAL of feat(tui): redesign Activity and Live panels in ActivityVisualizer #551 (Activity/Live panel redesign)
- Affected file:
apps/mcp-server/src/tui/components/activity-visualizer.pure.ts(lines 23–77) - Current tree rendering: depth=1 only (root + direct children)
- Related:
Edgetype fromapps/mcp-server/src/tui/dashboard-types.ts - Test file:
apps/mcp-server/src/tui/components/activity-visualizer.pure.spec.ts