An AI agent built to do Ralph loops - plan mode for planning and ralph mode for implementing.
at new-directions 206 lines 4.7 kB view raw
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>