An AI agent built to do Ralph loops - plan mode for planning and ralph mode for implementing.
1<script lang="ts">
2 /**
3 * TreeNodeRow component.
4 * Renders a single row in the hierarchical task tree.
5 * Shows title, status/priority badges, agent assignment, dependencies, and expand/collapse toggle.
6 */
7
8 import type { TreeNode } from '../lib/tree';
9 import StatusBadge from './StatusBadge.svelte';
10 import PriorityBadge from './PriorityBadge.svelte';
11 import TreeNodeRow from './TreeNodeRow.svelte';
12
13 type Props = {
14 treeNode: TreeNode;
15 depth: number;
16 selectedNodeId?: string | null;
17 onSelect?: (nodeId: string) => void;
18 onToggleExpanded?: (nodeId: string) => void;
19 };
20
21 let { treeNode, depth, selectedNodeId = null, onSelect, onToggleExpanded }: Props = $props();
22
23 const node = $derived(treeNode.node);
24 const hasChildren = $derived(treeNode.children.length > 0);
25 const hasDependencies = $derived(treeNode.dependencies.length > 0);
26 const isSelected = $derived(selectedNodeId === node.id);
27
28 function handleClick(): void {
29 onSelect?.(node.id);
30 }
31
32 function handleKeyDown(e: KeyboardEvent): void {
33 if (e.key === 'Enter' || e.key === ' ') {
34 e.preventDefault();
35 handleClick();
36 }
37 }
38
39 function handleToggle(e: Event): void {
40 e.stopPropagation();
41 onToggleExpanded?.(node.id);
42 }
43
44 const indentStyle = $derived(`padding-left: ${depth * 24}px`);
45 const isReady = $derived(node.status === 'ready');
46 const rowClass = $derived(`tree-row ${isSelected ? 'selected' : ''} ${isReady ? 'ready' : ''}`);
47</script>
48
49<div class={rowClass} style={indentStyle} role="button" tabindex="0" onclick={handleClick} onkeydown={handleKeyDown}>
50 <div class="row-content">
51 {#if hasChildren}
52 <button class="expand-toggle" onclick={handleToggle} aria-label="Toggle children">
53 <span class="toggle-arrow" class:expanded={treeNode.expanded}>▶</span>
54 </button>
55 {:else}
56 <span class="expand-placeholder"></span>
57 {/if}
58
59 <div class="node-info">
60 <span class="node-title">{node.title}</span>
61
62 <div class="badges">
63 <StatusBadge status={node.status} />
64 <PriorityBadge priority={node.priority} />
65 </div>
66
67 {#if node.assigned_to}
68 <span class="agent-label">{node.assigned_to}</span>
69 {/if}
70
71 {#if hasDependencies}
72 <span class="dependency-indicator">
73 depends on: {treeNode.dependencies.map((d) => d.id).join(', ')}
74 </span>
75 {/if}
76 </div>
77 </div>
78</div>
79
80{#if treeNode.expanded && hasChildren}
81 {#each treeNode.children as child (child.node.id)}
82 <TreeNodeRow
83 treeNode={child}
84 depth={depth + 1}
85 {selectedNodeId}
86 {onSelect}
87 {onToggleExpanded}
88 />
89 {/each}
90{/if}
91
92<style>
93 .tree-row {
94 display: flex;
95 flex-direction: column;
96 cursor: pointer;
97 user-select: none;
98 border-left: 2px solid transparent;
99 transition: background-color 0.15s ease, border-color 0.15s ease;
100 font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
101 font-size: 0.875rem;
102 }
103
104 .tree-row:nth-child(odd) {
105 background-color: #f9f9f9;
106 }
107
108 .tree-row:nth-child(even) {
109 background-color: #ffffff;
110 }
111
112 .tree-row:hover {
113 background-color: #f0f0f0;
114 }
115
116 .tree-row.selected {
117 background-color: #e0e7ff;
118 border-left-color: #3b82f6;
119 }
120
121 .tree-row.ready {
122 background-color: #ecfdf5;
123 }
124
125 .row-content {
126 display: flex;
127 align-items: center;
128 gap: 0.5rem;
129 padding: 0.5rem 0.75rem;
130 }
131
132 .expand-toggle {
133 background: none;
134 border: none;
135 cursor: pointer;
136 padding: 0;
137 width: 1.5rem;
138 height: 1.5rem;
139 display: flex;
140 align-items: center;
141 justify-content: center;
142 color: #6b7280;
143 transition: color 0.15s ease;
144 }
145
146 .expand-toggle:hover {
147 color: #374151;
148 }
149
150 .toggle-arrow {
151 display: inline-block;
152 transition: transform 0.15s ease;
153 transform: rotate(0deg);
154 font-size: 0.75rem;
155 }
156
157 .toggle-arrow.expanded {
158 transform: rotate(90deg);
159 }
160
161 .expand-placeholder {
162 width: 1.5rem;
163 }
164
165 .node-info {
166 display: flex;
167 align-items: center;
168 gap: 0.75rem;
169 flex: 1;
170 min-width: 0;
171 }
172
173 .node-title {
174 font-weight: 500;
175 color: #111827;
176 white-space: nowrap;
177 overflow: hidden;
178 text-overflow: ellipsis;
179 }
180
181 .badges {
182 display: flex;
183 gap: 0.375rem;
184 flex-shrink: 0;
185 }
186
187 .agent-label {
188 background-color: #dbeafe;
189 color: #1e40af;
190 padding: 0.125rem 0.5rem;
191 border-radius: 0.25rem;
192 font-size: 0.75rem;
193 font-weight: 500;
194 flex-shrink: 0;
195 }
196
197 .dependency-indicator {
198 background-color: #fee2e2;
199 color: #991b1b;
200 padding: 0.125rem 0.5rem;
201 border-radius: 0.25rem;
202 font-size: 0.75rem;
203 font-weight: 500;
204 flex-shrink: 0;
205 }
206</style>