@ariso-ai/ivan 1.0.31 → 1.0.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/dist/database/migration.d.ts +2 -2
  2. package/dist/database/migration.d.ts.map +1 -1
  3. package/dist/database/migration.js.map +1 -1
  4. package/dist/database/migrations/017_create_pr_reviews_table.d.ts +3 -0
  5. package/dist/database/migrations/017_create_pr_reviews_table.d.ts.map +1 -0
  6. package/dist/database/migrations/017_create_pr_reviews_table.js +23 -0
  7. package/dist/database/migrations/017_create_pr_reviews_table.js.map +1 -0
  8. package/dist/database/migrations/index.d.ts.map +1 -1
  9. package/dist/database/migrations/index.js +3 -1
  10. package/dist/database/migrations/index.js.map +1 -1
  11. package/dist/database/types.d.ts +13 -0
  12. package/dist/database/types.d.ts.map +1 -1
  13. package/dist/index.js +27 -0
  14. package/dist/index.js.map +1 -1
  15. package/dist/learnings/database.d.ts.map +1 -1
  16. package/dist/learnings/database.js.map +1 -1
  17. package/dist/services/address-executor.d.ts.map +1 -1
  18. package/dist/services/address-executor.js +3 -2
  19. package/dist/services/address-executor.js.map +1 -1
  20. package/dist/services/git-interfaces.d.ts +3 -3
  21. package/dist/services/git-interfaces.d.ts.map +1 -1
  22. package/dist/services/pr-service-cli.d.ts +3 -3
  23. package/dist/services/pr-service-cli.d.ts.map +1 -1
  24. package/dist/services/pr-service-cli.js +7 -7
  25. package/dist/services/pr-service-cli.js.map +1 -1
  26. package/dist/services/pr-service-pat.d.ts +3 -3
  27. package/dist/services/pr-service-pat.d.ts.map +1 -1
  28. package/dist/services/pr-service-pat.js +7 -7
  29. package/dist/services/pr-service-pat.js.map +1 -1
  30. package/dist/services/review-executor.d.ts +2 -1
  31. package/dist/services/review-executor.d.ts.map +1 -1
  32. package/dist/services/review-executor.js +135 -114
  33. package/dist/services/review-executor.js.map +1 -1
  34. package/dist/web-server.d.ts +1 -0
  35. package/dist/web-server.d.ts.map +1 -1
  36. package/dist/web-server.js +393 -200
  37. package/dist/web-server.js.map +1 -1
  38. package/package.json +1 -1
@@ -20,6 +20,7 @@ export class WebServer {
20
20
  // API Routes
21
21
  this.app.get('/api/jobs', this.getJobs.bind(this));
22
22
  this.app.get('/api/jobs/:jobId/tasks', this.getJobTasks.bind(this));
23
+ this.app.get('/api/jobs/:jobId/reviews', this.getJobReviews.bind(this));
23
24
  // Serve the main HTML page for all non-API routes
24
25
  this.app.get('/', (req, res) => {
25
26
  res.send(this.getMainHTML());
@@ -65,6 +66,31 @@ export class WebServer {
65
66
  res.status(500).json({ error: 'Failed to fetch job tasks' });
66
67
  }
67
68
  }
69
+ async getJobReviews(req, res) {
70
+ try {
71
+ const { jobId } = req.params;
72
+ const db = this.dbManager.getKysely();
73
+ const job = await db
74
+ .selectFrom('jobs')
75
+ .selectAll()
76
+ .where('uuid', '=', jobId)
77
+ .executeTakeFirst();
78
+ if (!job) {
79
+ res.status(404).json({ error: 'Job not found' });
80
+ return;
81
+ }
82
+ const reviews = await db
83
+ .selectFrom('pr_reviews')
84
+ .selectAll()
85
+ .where('job_uuid', '=', jobId)
86
+ .execute();
87
+ res.json({ job, reviews });
88
+ }
89
+ catch (error) {
90
+ console.error('Error fetching job reviews:', error);
91
+ res.status(500).json({ error: 'Failed to fetch job reviews' });
92
+ }
93
+ }
68
94
  getMainHTML() {
69
95
  return `<!DOCTYPE html>
70
96
  <html lang="en">
@@ -72,6 +98,7 @@ export class WebServer {
72
98
  <meta charset="UTF-8">
73
99
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
74
100
  <title>Ivan - Job Dashboard</title>
101
+ <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
75
102
  <style>
76
103
  * {
77
104
  margin: 0;
@@ -79,265 +106,378 @@ export class WebServer {
79
106
  box-sizing: border-box;
80
107
  }
81
108
 
109
+ html, body {
110
+ height: 100%;
111
+ }
112
+
82
113
  body {
83
114
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
84
115
  line-height: 1.6;
85
116
  color: #333;
86
117
  background: #f5f7fa;
87
- }
88
-
89
- .container {
90
- max-width: 1200px;
91
- margin: 0 auto;
92
- padding: 20px;
93
- }
94
-
95
- .header {
96
- background: white;
97
- padding: 20px 30px;
98
- border-radius: 12px;
99
- box-shadow: 0 2px 10px rgba(0,0,0,0.05);
100
- margin-bottom: 30px;
101
- }
102
-
103
- .header h1 {
104
- color: #2c3e50;
105
- font-size: 2rem;
106
- margin-bottom: 10px;
107
- }
108
-
109
- .header p {
110
- color: #7f8c8d;
111
- font-size: 1.1rem;
118
+ display: flex;
119
+ flex-direction: column;
112
120
  }
113
121
 
114
122
  .main-content {
115
123
  display: flex;
116
- gap: 30px;
124
+ flex: 1;
125
+ min-height: 0;
126
+ gap: 0;
117
127
  }
118
128
 
119
129
  .jobs-list {
120
- flex: 1;
130
+ width: 300px;
131
+ flex-shrink: 0;
121
132
  background: white;
122
- border-radius: 12px;
123
- box-shadow: 0 2px 10px rgba(0,0,0,0.05);
133
+ border-right: 1px solid #e1e4e8;
134
+ display: flex;
135
+ flex-direction: column;
124
136
  overflow: hidden;
125
137
  }
126
138
 
139
+ .jobs-scroll {
140
+ flex: 1;
141
+ overflow-y: auto;
142
+ }
143
+
127
144
  .job-detail {
128
- flex: 2;
145
+ flex: 1;
146
+ min-width: 0;
129
147
  background: white;
130
- border-radius: 12px;
131
- box-shadow: 0 2px 10px rgba(0,0,0,0.05);
132
- overflow: hidden;
133
148
  display: none;
149
+ flex-direction: column;
150
+ overflow: hidden;
151
+ }
152
+
153
+ .job-detail.visible {
154
+ display: flex;
134
155
  }
135
156
 
136
157
  .section-header {
137
- background: #3498db;
158
+ background: #24292f;
138
159
  color: white;
139
- padding: 20px 30px;
140
- font-size: 1.2rem;
160
+ padding: 14px 20px;
161
+ font-size: 0.95rem;
141
162
  font-weight: 600;
163
+ flex-shrink: 0;
142
164
  }
143
165
 
144
166
  .job-item {
145
- padding: 20px 30px;
146
- border-bottom: 1px solid #ecf0f1;
167
+ padding: 14px 20px;
168
+ border-bottom: 1px solid #f0f0f0;
147
169
  cursor: pointer;
148
- transition: background 0.2s ease;
170
+ transition: background 0.15s ease;
149
171
  }
150
172
 
151
- .job-item:hover {
152
- background: #f8f9fa;
153
- }
173
+ .job-item:hover { background: #f6f8fa; }
154
174
 
155
175
  .job-item.active {
156
- background: #e3f2fd;
157
- border-left: 4px solid #3498db;
176
+ background: #dbeafe;
177
+ border-left: 3px solid #3b82f6;
158
178
  }
159
179
 
160
- .job-item:last-child {
161
- border-bottom: none;
162
- }
180
+ .job-item:last-child { border-bottom: none; }
163
181
 
164
182
  .job-title {
165
183
  font-weight: 600;
166
- color: #2c3e50;
167
- margin-bottom: 5px;
184
+ font-size: 0.875rem;
185
+ color: #24292f;
186
+ margin-bottom: 3px;
187
+ }
188
+
189
+ .job-badge {
190
+ display: inline-block;
191
+ font-size: 0.65rem;
192
+ padding: 1px 5px;
193
+ border-radius: 6px;
194
+ margin-left: 5px;
195
+ vertical-align: middle;
196
+ background: #7c3aed;
197
+ color: white;
198
+ font-weight: 600;
199
+ letter-spacing: 0.02em;
168
200
  }
169
201
 
170
202
  .job-meta {
171
- font-size: 0.9rem;
172
- color: #7f8c8d;
203
+ font-size: 0.75rem;
204
+ color: #6e7781;
173
205
  }
174
206
 
175
207
  .job-detail-content {
176
208
  display: flex;
177
- height: 100%;
209
+ flex: 1;
210
+ min-height: 0;
178
211
  }
179
212
 
180
213
  .tasks-sidebar {
181
- width: 350px;
182
- border-right: 1px solid #ecf0f1;
214
+ width: 300px;
215
+ flex-shrink: 0;
216
+ border-right: 1px solid #e1e4e8;
217
+ display: flex;
218
+ flex-direction: column;
219
+ overflow: hidden;
220
+ }
221
+
222
+ .sidebar-tabs {
223
+ display: flex;
224
+ border-bottom: 1px solid #e1e4e8;
225
+ flex-shrink: 0;
226
+ }
227
+
228
+ .sidebar-tab {
229
+ flex: 1;
230
+ padding: 10px;
231
+ text-align: center;
232
+ cursor: pointer;
233
+ font-size: 0.8rem;
234
+ font-weight: 500;
235
+ color: #6e7781;
236
+ border-bottom: 2px solid transparent;
237
+ margin-bottom: -1px;
238
+ transition: color 0.15s;
239
+ }
240
+
241
+ .sidebar-tab:hover { color: #24292f; }
242
+
243
+ .sidebar-tab.active {
244
+ color: #0969da;
245
+ border-bottom-color: #0969da;
246
+ }
247
+
248
+ .sidebar-scroll {
249
+ flex: 1;
183
250
  overflow-y: auto;
184
251
  }
185
252
 
186
253
  .task-content {
187
254
  flex: 1;
255
+ min-width: 0;
188
256
  overflow-y: auto;
189
- padding: 30px;
257
+ padding: 30px 40px;
190
258
  }
191
259
 
192
- .task-item {
193
- padding: 15px 20px;
194
- border-bottom: 1px solid #ecf0f1;
260
+ .task-item, .review-item {
261
+ padding: 12px 16px;
262
+ border-bottom: 1px solid #f0f0f0;
195
263
  cursor: pointer;
196
- transition: background 0.2s ease;
264
+ transition: background 0.15s ease;
197
265
  }
198
266
 
199
- .task-item:hover {
200
- background: #f8f9fa;
201
- }
267
+ .task-item:hover, .review-item:hover { background: #f6f8fa; }
202
268
 
203
269
  .task-item.active {
204
- background: #e8f5e8;
205
- border-left: 4px solid #27ae60;
270
+ background: #dcfce7;
271
+ border-left: 3px solid #16a34a;
206
272
  }
207
273
 
208
- .task-title {
209
- font-weight: 500;
210
- color: #2c3e50;
211
- margin-bottom: 5px;
274
+ .review-item.active {
275
+ background: #ede9fe;
276
+ border-left: 3px solid #7c3aed;
212
277
  }
213
278
 
214
- .task-status {
215
- font-size: 0.8rem;
216
- padding: 2px 8px;
217
- border-radius: 12px;
279
+ .task-title, .review-title {
218
280
  font-weight: 500;
281
+ font-size: 0.85rem;
282
+ color: #24292f;
283
+ margin-bottom: 4px;
219
284
  }
220
285
 
221
- .status-not_started {
222
- background: #ffeaa7;
223
- color: #d68910;
224
- }
225
-
226
- .status-active {
227
- background: #74b9ff;
228
- color: #0984e3;
286
+ .task-status {
287
+ display: inline-block;
288
+ font-size: 0.7rem;
289
+ padding: 1px 7px;
290
+ border-radius: 10px;
291
+ font-weight: 600;
229
292
  }
230
293
 
231
- .status-completed {
232
- background: #00b894;
233
- color: white;
234
- }
294
+ .status-not_started { background: #fef9c3; color: #854d0e; }
295
+ .status-active { background: #dbeafe; color: #1d4ed8; }
296
+ .status-completed { background: #dcfce7; color: #15803d; }
297
+ .status-failed { background: #fee2e2; color: #b91c1c; }
235
298
 
236
- .task-detail-header {
237
- border-bottom: 1px solid #ecf0f1;
299
+ .detail-header {
238
300
  padding-bottom: 20px;
239
- margin-bottom: 20px;
301
+ margin-bottom: 24px;
302
+ border-bottom: 1px solid #e1e4e8;
240
303
  }
241
304
 
242
- .task-detail-title {
243
- font-size: 1.3rem;
244
- color: #2c3e50;
245
- margin-bottom: 10px;
305
+ .detail-title {
306
+ font-size: 1.25rem;
307
+ font-weight: 600;
308
+ color: #24292f;
309
+ margin-bottom: 8px;
246
310
  }
247
311
 
248
312
  .pr-link {
249
313
  display: inline-block;
250
- background: #3498db;
314
+ background: #0969da;
251
315
  color: white;
252
- padding: 8px 16px;
316
+ padding: 6px 14px;
253
317
  border-radius: 6px;
254
318
  text-decoration: none;
255
- font-size: 0.9rem;
256
- margin-top: 10px;
319
+ font-size: 0.85rem;
320
+ margin-top: 8px;
321
+ font-weight: 500;
257
322
  }
258
323
 
259
- .pr-link:hover {
260
- background: #2980b9;
261
- }
324
+ .pr-link:hover { background: #0860ca; }
262
325
 
263
326
  .execution-log {
264
- background: #2c3e50;
265
- color: #ecf0f1;
327
+ background: #161b22;
328
+ color: #c9d1d9;
266
329
  padding: 20px;
267
330
  border-radius: 8px;
268
- font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
269
- font-size: 0.9rem;
270
- line-height: 1.4;
331
+ font-family: 'SF Mono', 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
332
+ font-size: 0.825rem;
333
+ line-height: 1.5;
271
334
  white-space: pre-wrap;
272
335
  overflow-x: auto;
273
- max-height: 500px;
274
- overflow-y: auto;
275
336
  }
276
337
 
338
+ /* Markdown-rendered review output */
339
+ .review-markdown {
340
+ font-size: 0.95rem;
341
+ line-height: 1.7;
342
+ color: #24292f;
343
+ }
344
+
345
+ .review-markdown h1,
346
+ .review-markdown h2,
347
+ .review-markdown h3,
348
+ .review-markdown h4 {
349
+ margin-top: 1.5em;
350
+ margin-bottom: 0.5em;
351
+ font-weight: 600;
352
+ color: #24292f;
353
+ }
354
+
355
+ .review-markdown h1 { font-size: 1.4rem; border-bottom: 1px solid #e1e4e8; padding-bottom: 0.3em; }
356
+ .review-markdown h2 { font-size: 1.15rem; border-bottom: 1px solid #e1e4e8; padding-bottom: 0.2em; }
357
+ .review-markdown h3 { font-size: 1rem; }
358
+ .review-markdown h4 { font-size: 0.9rem; }
359
+
360
+ .review-markdown p { margin-bottom: 1em; }
361
+
362
+ .review-markdown ul, .review-markdown ol {
363
+ margin: 0.5em 0 1em 1.5em;
364
+ }
365
+
366
+ .review-markdown li { margin-bottom: 0.25em; }
367
+
368
+ .review-markdown code {
369
+ background: #f6f8fa;
370
+ border: 1px solid #e1e4e8;
371
+ border-radius: 4px;
372
+ padding: 0.1em 0.4em;
373
+ font-family: 'SF Mono', 'Monaco', 'Menlo', monospace;
374
+ font-size: 0.85em;
375
+ }
376
+
377
+ .review-markdown pre {
378
+ background: #161b22;
379
+ border-radius: 8px;
380
+ padding: 16px;
381
+ overflow-x: auto;
382
+ margin: 1em 0;
383
+ }
384
+
385
+ .review-markdown pre code {
386
+ background: none;
387
+ border: none;
388
+ padding: 0;
389
+ color: #c9d1d9;
390
+ font-size: 0.85rem;
391
+ line-height: 1.5;
392
+ }
393
+
394
+ .review-markdown blockquote {
395
+ border-left: 4px solid #d0d7de;
396
+ padding-left: 1em;
397
+ color: #6e7781;
398
+ margin: 1em 0;
399
+ }
400
+
401
+ .review-markdown hr {
402
+ border: none;
403
+ border-top: 1px solid #e1e4e8;
404
+ margin: 1.5em 0;
405
+ }
406
+
407
+ .review-markdown strong { font-weight: 600; }
408
+
409
+ .review-markdown a { color: #0969da; text-decoration: none; }
410
+ .review-markdown a:hover { text-decoration: underline; }
411
+
277
412
  .empty-state {
278
413
  padding: 60px 30px;
279
414
  text-align: center;
280
- color: #7f8c8d;
415
+ color: #6e7781;
281
416
  }
282
417
 
283
418
  .empty-state h3 {
284
- margin-bottom: 10px;
285
- color: #95a5a6;
419
+ margin-bottom: 8px;
420
+ color: #8c959f;
421
+ font-weight: 500;
286
422
  }
287
423
 
288
424
  .loading {
289
- padding: 60px 30px;
425
+ padding: 40px 20px;
290
426
  text-align: center;
291
- color: #7f8c8d;
427
+ color: #6e7781;
428
+ font-size: 0.875rem;
292
429
  }
293
430
 
294
- @media (max-width: 768px) {
295
- .main-content {
296
- flex-direction: column;
297
- height: auto;
298
- }
299
-
300
- .job-detail-content {
301
- flex-direction: column;
302
- }
431
+ .section-label {
432
+ font-size: 0.75rem;
433
+ font-weight: 600;
434
+ color: #6e7781;
435
+ text-transform: uppercase;
436
+ letter-spacing: 0.05em;
437
+ margin-bottom: 12px;
438
+ }
303
439
 
304
- .tasks-sidebar {
305
- width: 100%;
306
- max-height: 300px;
307
- }
440
+ @media (max-width: 900px) {
441
+ .main-content { flex-direction: column; }
442
+ .jobs-list { width: 100%; border-right: none; border-bottom: 1px solid #e1e4e8; max-height: 250px; }
443
+ .tasks-sidebar { width: 100%; border-right: none; border-bottom: 1px solid #e1e4e8; max-height: 220px; }
444
+ .job-detail-content { flex-direction: column; }
308
445
  }
309
446
  </style>
310
447
  </head>
311
448
  <body>
312
- <div class="container">
313
- <div class="main-content">
314
- <div class="jobs-list">
315
- <div class="section-header">
316
- Jobs
317
- </div>
318
- <div id="jobs-container">
319
- <div class="loading">Loading jobs...</div>
320
- </div>
449
+ <div class="main-content">
450
+ <div class="jobs-list">
451
+ <div class="section-header">Jobs</div>
452
+ <div class="jobs-scroll" id="jobs-container">
453
+ <div class="loading">Loading jobs...</div>
321
454
  </div>
455
+ </div>
322
456
 
323
- <div class="job-detail" id="job-detail">
324
- <div class="section-header">
325
- <span id="job-detail-title">Job Details</span>
326
- </div>
327
- <div class="job-detail-content">
328
- <div class="tasks-sidebar">
457
+ <div class="job-detail" id="job-detail">
458
+ <div class="section-header">
459
+ <span id="job-detail-title">Job Details</span>
460
+ </div>
461
+ <div class="job-detail-content">
462
+ <div class="tasks-sidebar">
463
+ <div class="sidebar-tabs">
464
+ <div class="sidebar-tab active" id="tab-tasks" onclick="switchTab('tasks')">Tasks</div>
465
+ <div class="sidebar-tab" id="tab-reviews" onclick="switchTab('reviews')">Reviews</div>
466
+ </div>
467
+ <div class="sidebar-scroll">
329
468
  <div id="tasks-container">
330
- <div class="empty-state">
331
- <h3>Select a job to view tasks</h3>
332
- </div>
469
+ <div class="empty-state"><h3>Select a job</h3></div>
470
+ </div>
471
+ <div id="reviews-container" style="display:none;">
472
+ <div class="empty-state"><h3>Select a job</h3></div>
333
473
  </div>
334
474
  </div>
335
- <div class="task-content">
336
- <div id="task-detail-container">
337
- <div class="empty-state">
338
- <h3>Select a task to view details</h3>
339
- <p>Choose a task from the sidebar to see its execution log and details.</p>
340
- </div>
475
+ </div>
476
+ <div class="task-content">
477
+ <div id="detail-container">
478
+ <div class="empty-state">
479
+ <h3>Select an item to view details</h3>
480
+ <p>Choose a task or review from the sidebar.</p>
341
481
  </div>
342
482
  </div>
343
483
  </div>
@@ -349,8 +489,11 @@ export class WebServer {
349
489
  let jobs = [];
350
490
  let currentJob = null;
351
491
  let currentTasks = [];
492
+ let currentReviews = [];
493
+ let activeTab = 'tasks';
494
+
495
+ marked.setOptions({ breaks: true, gfm: true });
352
496
 
353
- // Load jobs on page load
354
497
  loadJobs();
355
498
 
356
499
  async function loadJobs() {
@@ -365,93 +508,143 @@ export class WebServer {
365
508
  }
366
509
  }
367
510
 
511
+ function esc(str) {
512
+ return String(str)
513
+ .replace(/&/g, '&amp;')
514
+ .replace(/</g, '&lt;')
515
+ .replace(/>/g, '&gt;')
516
+ .replace(/"/g, '&quot;');
517
+ }
518
+
368
519
  function renderJobs() {
369
520
  const container = document.getElementById('jobs-container');
370
-
371
521
  if (jobs.length === 0) {
372
- container.innerHTML = '<div class="empty-state"><h3>No jobs found</h3><p>Create some tasks with Ivan CLI to see them here.</p></div>';
522
+ container.innerHTML = '<div class="empty-state"><h3>No jobs yet</h3><p>Run some Ivan commands to see jobs here.</p></div>';
373
523
  return;
374
524
  }
375
-
376
525
  container.innerHTML = jobs.map(job => {
377
526
  const date = new Date(job.created_at).toLocaleString();
378
- return \`
379
- <div class="job-item" onclick="selectJob('\${job.uuid}')">
380
- <div class="job-title">\${job.description}</div>
381
- <div class="job-meta">
382
- <div>Created: \${date}</div>
383
- <div>Directory: \${job.directory}</div>
384
- </div>
385
- </div>
386
- \`;
527
+ const isReview = job.description.startsWith('PR Review -');
528
+ const badge = isReview ? '<span class="job-badge">review</span>' : '';
529
+ return \`<div class="job-item" onclick="selectJob('\${esc(job.uuid)}', event)">
530
+ <div class="job-title">\${esc(job.description)}\${badge}</div>
531
+ <div class="job-meta">\${date}</div>
532
+ </div>\`;
387
533
  }).join('');
388
534
  }
389
535
 
390
- async function selectJob(jobId) {
391
- // Update job item selection
392
- document.querySelectorAll('.job-item').forEach(item => item.classList.remove('active'));
393
- event.target.closest('.job-item').classList.add('active');
536
+ async function selectJob(jobId, evt) {
537
+ document.querySelectorAll('.job-item').forEach(el => el.classList.remove('active'));
538
+ evt.target.closest('.job-item').classList.add('active');
394
539
 
395
540
  try {
396
- const response = await fetch(\`/api/jobs/\${jobId}/tasks\`);
397
- const data = await response.json();
398
-
399
- currentJob = data.job;
400
- currentTasks = data.tasks;
401
-
402
- document.getElementById('job-detail-title').textContent = \`\${data.job.description}\`;
403
- document.getElementById('job-detail').style.display = 'block';
404
-
405
- renderTasks();
406
-
407
- // Clear task detail
408
- document.getElementById('task-detail-container').innerHTML =
409
- '<div class="empty-state"><h3>Select a task to view details</h3><p>Choose a task from the sidebar to see its execution log and details.</p></div>';
410
-
411
- // Scroll to top of page
412
- window.scrollTo({ top: 0, behavior: 'smooth' });
541
+ const [tasksResp, reviewsResp] = await Promise.all([
542
+ fetch(\`/api/jobs/\${jobId}/tasks\`),
543
+ fetch(\`/api/jobs/\${jobId}/reviews\`)
544
+ ]);
545
+ const tasksData = await tasksResp.json();
546
+ const reviewsData = await reviewsResp.json();
547
+
548
+ currentJob = tasksData.job;
549
+ currentTasks = tasksData.tasks;
550
+ currentReviews = reviewsData.reviews || [];
551
+
552
+ document.getElementById('job-detail-title').textContent = currentJob.description;
553
+ document.getElementById('job-detail').classList.add('visible');
554
+
555
+ if (currentReviews.length > 0 && currentTasks.length === 0) {
556
+ switchTab('reviews');
557
+ } else {
558
+ switchTab('tasks');
559
+ }
413
560
 
561
+ document.getElementById('detail-container').innerHTML =
562
+ '<div class="empty-state"><h3>Select an item to view details</h3><p>Choose a task or review from the sidebar.</p></div>';
414
563
  } catch (error) {
415
- console.error('Failed to load job tasks:', error);
564
+ console.error('Failed to load job data:', error);
416
565
  }
417
566
  }
418
567
 
568
+ function switchTab(tab) {
569
+ activeTab = tab;
570
+ document.getElementById('tab-tasks').classList.toggle('active', tab === 'tasks');
571
+ document.getElementById('tab-reviews').classList.toggle('active', tab === 'reviews');
572
+ document.getElementById('tasks-container').style.display = tab === 'tasks' ? '' : 'none';
573
+ document.getElementById('reviews-container').style.display = tab === 'reviews' ? '' : 'none';
574
+ if (tab === 'tasks') renderTasks();
575
+ else renderReviews();
576
+ }
577
+
419
578
  function renderTasks() {
420
579
  const container = document.getElementById('tasks-container');
421
-
422
580
  if (currentTasks.length === 0) {
423
- container.innerHTML = '<div class="empty-state"><h3>No tasks found</h3></div>';
581
+ container.innerHTML = '<div class="empty-state"><h3>No tasks</h3></div>';
424
582
  return;
425
583
  }
426
-
427
584
  container.innerHTML = currentTasks.map(task => \`
428
- <div class="task-item" onclick="selectTask('\${task.uuid}')">
429
- <div class="task-title">\${task.description}</div>
430
- <div class="task-status status-\${task.status}">\${task.status.replace('_', ' ')}</div>
585
+ <div class="task-item" onclick="selectTask('\${esc(task.uuid)}')">
586
+ <div class="task-title">\${esc(task.description)}</div>
587
+ <span class="task-status status-\${task.status}">\${task.status.replace('_', ' ')}</span>
588
+ </div>
589
+ \`).join('');
590
+ }
591
+
592
+ function renderReviews() {
593
+ const container = document.getElementById('reviews-container');
594
+ if (currentReviews.length === 0) {
595
+ container.innerHTML = '<div class="empty-state"><h3>No reviews</h3></div>';
596
+ return;
597
+ }
598
+ container.innerHTML = currentReviews.map(review => \`
599
+ <div class="review-item" onclick="selectReview('\${esc(review.uuid)}')">
600
+ <div class="review-title">PR #\${review.pr_number}\${review.pr_title ? ': ' + esc(review.pr_title) : ''}</div>
601
+ <span class="task-status status-\${review.status}">\${review.status.replace('_', ' ')}</span>
431
602
  </div>
432
603
  \`).join('');
433
604
  }
434
605
 
435
606
  function selectTask(taskId) {
436
- // Update task item selection
437
- document.querySelectorAll('.task-item').forEach(item => item.classList.remove('active'));
607
+ document.querySelectorAll('.task-item').forEach(el => el.classList.remove('active'));
438
608
  event.target.closest('.task-item').classList.add('active');
439
609
 
440
610
  const task = currentTasks.find(t => t.uuid === taskId);
441
611
  if (!task) return;
442
612
 
443
- const container = document.getElementById('task-detail-container');
613
+ document.getElementById('detail-container').innerHTML = \`
614
+ <div class="detail-header">
615
+ <div class="detail-title">\${esc(task.description)}</div>
616
+ <span class="task-status status-\${task.status}">\${task.status.replace('_', ' ')}</span>
617
+ \${task.pr_link ? \`<br><a href="\${esc(task.pr_link)}" target="_blank" class="pr-link">View Pull Request</a>\` : ''}
618
+ </div>
619
+ \${task.execution_log
620
+ ? \`<div class="section-label">Execution Log</div><div class="execution-log">\${esc(task.execution_log)}</div>\`
621
+ : '<div class="empty-state"><h3>No execution log</h3><p>This task has not run yet.</p></div>'}
622
+ \`;
623
+ }
624
+
625
+ function selectReview(reviewId) {
626
+ document.querySelectorAll('.review-item').forEach(el => el.classList.remove('active'));
627
+ event.target.closest('.review-item').classList.add('active');
628
+
629
+ const review = currentReviews.find(r => r.uuid === reviewId);
630
+ if (!review) return;
631
+
632
+ let bodyHtml;
633
+ if (review.review_output) {
634
+ bodyHtml = \`<div class="section-label">Review</div><div class="review-markdown">\${marked.parse(review.review_output)}</div>\`;
635
+ } else if (review.review_log) {
636
+ bodyHtml = \`<div class="section-label">Log</div><div class="execution-log">\${esc(review.review_log)}</div>\`;
637
+ } else {
638
+ bodyHtml = '<div class="empty-state"><h3>No review output yet</h3><p>The review is still running or has not started.</p></div>';
639
+ }
444
640
 
445
- container.innerHTML = \`
446
- <div class="task-detail-header">
447
- <div class="task-detail-title">\${task.description}</div>
448
- <div class="task-status status-\${task.status}">\${task.status.replace('_', ' ')}</div>
449
- \${task.pr_link ? \`<a href="\${task.pr_link}" target="_blank" class="pr-link">View Pull Request</a>\` : ''}
641
+ document.getElementById('detail-container').innerHTML = \`
642
+ <div class="detail-header">
643
+ <div class="detail-title">PR #\${review.pr_number}\${review.pr_title ? ': ' + esc(review.pr_title) : ''}</div>
644
+ <span class="task-status status-\${review.status}">\${review.status.replace('_', ' ')}</span>
645
+ \${review.pr_url ? \`<br><a href="\${esc(review.pr_url)}" target="_blank" class="pr-link">View PR on GitHub</a>\` : ''}
450
646
  </div>
451
- \${task.execution_log ? \`
452
- <h3 style="margin-bottom: 15px; color: #2c3e50;">Execution Log</h3>
453
- <div class="execution-log">\${task.execution_log}</div>
454
- \` : '<div class="empty-state"><h3>No execution log</h3><p>This task hasn\\'t been executed yet or no log was recorded.</p></div>'}
647
+ \${bodyHtml}
455
648
  \`;
456
649
  }
457
650
  </script>