An AI agent built to do Ralph loops - plan mode for planning and ralph mode for implementing.

feat(web): add ProjectList view

Implements Phase 4d Task 3. Creates ProjectList.svelte to display all registered projects in a table with clickable project names that navigate to ProjectDetail. Shows project path in monospace and registration date in human-readable format. Handles loading, error, and empty states. Updates App.svelte to use ProjectList for project-list route instead of placeholder.

+239 -1
+2 -1
web/src/App.svelte
··· 12 12 import type { WsEvent } from './types'; 13 13 import Sidebar from './components/Sidebar.svelte'; 14 14 import Dashboard from './views/Dashboard.svelte'; 15 + import ProjectList from './views/ProjectList.svelte'; 15 16 import Placeholder from './views/Placeholder.svelte'; 16 17 17 18 let wsConnection: ReturnType<typeof createWsConnection> | null = null; ··· 51 52 {#if currentRoute.name === 'dashboard'} 52 53 <Dashboard /> 53 54 {:else if currentRoute.name === 'project-list'} 54 - <Placeholder name="Project List" /> 55 + <ProjectList /> 55 56 {:else if currentRoute.name === 'project-detail'} 56 57 <Placeholder name="Project Detail" /> 57 58 {:else if currentRoute.name === 'task-tree'}
+237
web/src/views/ProjectList.svelte
··· 1 + <script lang="ts"> 2 + /** 3 + * ProjectList view component. 4 + * Displays all registered projects in a table with navigation to ProjectDetail. 5 + */ 6 + 7 + import { loadProjects, projectsState } from '../stores/projects.svelte'; 8 + import { navigate } from '../router.svelte'; 9 + import LoadingSpinner from '../components/LoadingSpinner.svelte'; 10 + import ErrorMessage from '../components/ErrorMessage.svelte'; 11 + 12 + let loading = $state(true); 13 + let error = $state<string | null>(null); 14 + 15 + /** 16 + * Load projects on mount. 17 + */ 18 + $effect(async () => { 19 + try { 20 + loading = true; 21 + error = null; 22 + await loadProjects(); 23 + } catch (err) { 24 + error = err instanceof Error ? err.message : 'Unknown error'; 25 + } finally { 26 + loading = false; 27 + } 28 + }); 29 + 30 + /** 31 + * Format a date string into human-readable format. 32 + */ 33 + function formatDate(dateStr: string): string { 34 + try { 35 + const date = new Date(dateStr); 36 + return date.toLocaleDateString('en-US', { 37 + year: 'numeric', 38 + month: 'short', 39 + day: 'numeric', 40 + }); 41 + } catch { 42 + return dateStr; 43 + } 44 + } 45 + 46 + /** 47 + * Handle project row click - navigate to ProjectDetail. 48 + */ 49 + function handleProjectClick(projectId: string): void { 50 + navigate(`/projects/${projectId}`); 51 + } 52 + </script> 53 + 54 + <div class="project-list-container"> 55 + {#if loading} 56 + <div class="loading-state"> 57 + <LoadingSpinner /> 58 + <p>Loading projects...</p> 59 + </div> 60 + {:else if error} 61 + <ErrorMessage message={error} /> 62 + {:else if projectsState.projects.length === 0} 63 + <div class="empty-state"> 64 + <p>No projects registered.</p> 65 + <p class="instruction">Use `rustagent project add &lt;name&gt; &lt;path&gt;` to register a project.</p> 66 + </div> 67 + {:else} 68 + <div class="project-list-wrapper"> 69 + <h1>Projects</h1> 70 + <table class="projects-table"> 71 + <thead> 72 + <tr> 73 + <th class="name-col">Name</th> 74 + <th class="path-col">Path</th> 75 + <th class="date-col">Registered</th> 76 + </tr> 77 + </thead> 78 + <tbody> 79 + {#each projectsState.projects as project (project.id)} 80 + <tr class="project-row" onclick={() => handleProjectClick(project.id)}> 81 + <td class="name-cell"> 82 + <button class="project-name-link" onclick={() => handleProjectClick(project.id)}> 83 + {project.name} 84 + </button> 85 + </td> 86 + <td class="path-cell"> 87 + <code class="path-code">{project.path}</code> 88 + </td> 89 + <td class="date-cell"> 90 + {formatDate(project.registered_at)} 91 + </td> 92 + </tr> 93 + {/each} 94 + </tbody> 95 + </table> 96 + </div> 97 + {/if} 98 + </div> 99 + 100 + <style> 101 + .project-list-container { 102 + padding: 2rem; 103 + max-width: 1200px; 104 + margin: 0 auto; 105 + } 106 + 107 + .loading-state { 108 + display: flex; 109 + flex-direction: column; 110 + align-items: center; 111 + justify-content: center; 112 + gap: 1rem; 113 + padding: 4rem 2rem; 114 + text-align: center; 115 + color: #9ca3af; 116 + } 117 + 118 + .empty-state { 119 + display: flex; 120 + flex-direction: column; 121 + align-items: center; 122 + justify-content: center; 123 + gap: 1rem; 124 + padding: 4rem 2rem; 125 + text-align: center; 126 + color: #9ca3af; 127 + } 128 + 129 + .empty-state p { 130 + margin: 0; 131 + } 132 + 133 + .instruction { 134 + font-size: 0.875rem; 135 + color: #6b7280; 136 + } 137 + 138 + .project-list-wrapper { 139 + width: 100%; 140 + } 141 + 142 + .project-list-wrapper h1 { 143 + margin: 0 0 1.5rem 0; 144 + font-size: 1.875rem; 145 + font-weight: 700; 146 + color: #ffffff; 147 + } 148 + 149 + .projects-table { 150 + width: 100%; 151 + border-collapse: collapse; 152 + background-color: #242444; 153 + border: 1px solid #404055; 154 + border-radius: 0.5rem; 155 + overflow: hidden; 156 + } 157 + 158 + .projects-table thead { 159 + background-color: #1a1a2e; 160 + border-bottom: 1px solid #404055; 161 + } 162 + 163 + .projects-table th { 164 + padding: 1rem; 165 + text-align: left; 166 + font-size: 0.875rem; 167 + font-weight: 600; 168 + color: #d1d5db; 169 + text-transform: uppercase; 170 + letter-spacing: 0.05em; 171 + } 172 + 173 + .name-col { 174 + width: 30%; 175 + } 176 + 177 + .path-col { 178 + width: 50%; 179 + } 180 + 181 + .date-col { 182 + width: 20%; 183 + } 184 + 185 + .project-row { 186 + border-bottom: 1px solid #404055; 187 + transition: background-color 0.2s ease; 188 + cursor: pointer; 189 + } 190 + 191 + .project-row:hover { 192 + background-color: #2a2a3e; 193 + } 194 + 195 + .project-row:last-child { 196 + border-bottom: none; 197 + } 198 + 199 + .projects-table td { 200 + padding: 1rem; 201 + color: #e0e0e0; 202 + } 203 + 204 + .name-cell { 205 + font-weight: 500; 206 + } 207 + 208 + .project-name-link { 209 + background: none; 210 + border: none; 211 + color: #3b82f6; 212 + cursor: pointer; 213 + font-size: 0.95rem; 214 + font-weight: 500; 215 + padding: 0; 216 + text-decoration: none; 217 + } 218 + 219 + .project-name-link:hover { 220 + color: #60a5fa; 221 + text-decoration: underline; 222 + } 223 + 224 + .path-code { 225 + background-color: #1a1a2e; 226 + color: #10b981; 227 + padding: 0.25rem 0.5rem; 228 + border-radius: 0.25rem; 229 + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; 230 + font-size: 0.85rem; 231 + } 232 + 233 + .date-cell { 234 + font-size: 0.875rem; 235 + color: #9ca3af; 236 + } 237 + </style>