personal memory agent
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Refactor agents app: flat API, client-side grouping, unified agent view

Server now returns flat {runs, agents, facets} data. All grouping,
sorting, and aggregation moved to frontend. Shows all agents (generators
and tool agents) unified with capability badges, descriptions, color
accents, model info, error previews, and day summary bar.

- Expand _parse_agent_events() with model and error_message extraction
- Add _build_agents_meta() loading all muse configs (not just tool agents)
- Restructure api_agents_day() to return flat data + metadata
- Remove redundant api_agent_runs() endpoint and _group_agents_by_name()
- Rewrite workspace.html with client-side grouping and rich card UI

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

+337 -208
+45 -141
apps/agents/routes.py
··· 54 54 lines: List of JSONL lines (excluding the request event) 55 55 56 56 Returns: 57 - Dict with: thinking_count, tool_count, model, usage, finish_ts 57 + Dict with: thinking_count, tool_count, model, usage, finish_ts, 58 + error_message 58 59 """ 59 - result = { 60 + result: dict[str, Any] = { 60 61 "thinking_count": 0, 61 62 "tool_count": 0, 62 63 "model": None, 63 64 "usage": None, 64 65 "finish_ts": None, 66 + "error_message": None, 65 67 } 66 68 67 69 for line in lines: ··· 80 82 elif event_type == "finish": 81 83 result["finish_ts"] = event.get("ts", 0) 82 84 result["usage"] = event.get("usage") 85 + elif event_type == "error": 86 + msg = event.get("error", "") 87 + if msg: 88 + result["error_message"] = msg[:200] 83 89 except json.JSONDecodeError: 84 90 continue 85 91 ··· 114 120 """Parse agent JSONL file and extract metadata. 115 121 116 122 Returns dict with: id, name, start, status, prompt, facet, failed, 117 - runtime_seconds, thinking_count, tool_count, cost. 123 + runtime_seconds, thinking_count, tool_count, cost, model, error_message. 118 124 Returns None if file cannot be parsed. 119 125 """ 120 126 from think.cortex_client import get_agent_end_state ··· 141 147 # Parse events using shared helper 142 148 event_data = _parse_agent_events(lines[1:]) 143 149 144 - agent_info = { 150 + agent_info: dict[str, Any] = { 145 151 "id": agent_id, 146 152 "name": request_event.get("name", "default"), 147 153 "start": request_event.get("ts", 0), ··· 153 159 "thinking_count": event_data["thinking_count"], 154 160 "tool_count": event_data["tool_count"], 155 161 "cost": None, 162 + "model": event_data["model"], 163 + "error_message": event_data["error_message"], 156 164 } 157 165 158 166 # For completed agents, determine end state and calculate cost ··· 217 225 return agents 218 226 219 227 220 - def _group_agents_by_name( 221 - agents: list[dict], agents_meta: dict 222 - ) -> dict[str, dict[str, Any]]: 223 - """Group agents by name and add metadata. 228 + def _build_agents_meta() -> dict[str, dict[str, Any]]: 229 + """Build agent metadata dict from all muse configs. 224 230 225 - Returns dict mapping name to: 226 - - title: Display title 227 - - source: "system" or "app" 228 - - app: App name (for app agents) 229 - - run_count: Total runs 230 - - failed_count: Failed runs 231 - - thinking_count: Total thinking events across all runs 232 - - tool_count: Total tool calls across all runs 233 - - total_cost: Total cost in USD across all runs 234 - - facets: Set of facets with runs (for color hints) 231 + Returns dict mapping agent name to metadata with capability fields 232 + for frontend display. 235 233 """ 236 - groups: dict[str, dict[str, Any]] = {} 234 + configs = get_muse_configs(include_disabled=True) 235 + agents: dict[str, dict[str, Any]] = {} 237 236 238 - for agent in agents: 239 - name = agent["name"] 240 - if name not in groups: 241 - meta = agents_meta.get(name, {}) 242 - groups[name] = { 243 - "name": name, 244 - "title": meta.get("title", name), 245 - "source": meta.get("source", "system"), 246 - "app": meta.get("app"), 247 - "run_count": 0, 248 - "failed_count": 0, 249 - "thinking_count": 0, 250 - "tool_count": 0, 251 - "total_cost": 0.0, 252 - "facets": set(), 253 - } 254 - 255 - groups[name]["run_count"] += 1 256 - if agent.get("failed"): 257 - groups[name]["failed_count"] += 1 258 - groups[name]["thinking_count"] += agent.get("thinking_count", 0) 259 - groups[name]["tool_count"] += agent.get("tool_count", 0) 260 - if agent.get("cost") is not None: 261 - groups[name]["total_cost"] += agent["cost"] 262 - if agent.get("facet"): 263 - groups[name]["facets"].add(agent["facet"]) 237 + for name, config in configs.items(): 238 + agents[name] = { 239 + "title": config.get("title", name), 240 + "description": config.get("description"), 241 + "color": config.get("color", "#6c757d"), 242 + "source": config.get("source", "system"), 243 + "app": config.get("app"), 244 + "schedule": config.get("schedule"), 245 + "has_tools": "tools" in config, 246 + "has_output": "output" in config, 247 + "multi_facet": bool(config.get("multi_facet")), 248 + } 264 249 265 - # Convert facet sets to lists for JSON serialization 266 - for group in groups.values(): 267 - group["facets"] = list(group["facets"]) 268 - 269 - return groups 250 + return agents 270 251 271 252 272 253 # ============================================================================= ··· 299 280 300 281 @agents_bp.route("/api/agents/<day>") 301 282 def api_agents_day(day: str) -> Any: 302 - """Get agents that ran on a specific day, grouped by name. 283 + """Get agent runs and metadata for a specific day. 284 + 285 + Returns flat data for frontend grouping/rendering. 303 286 304 287 Query params: 305 288 facet: Optional facet filter (from cookie if not specified) 306 289 307 290 Returns: 308 291 { 309 - "groups": { 310 - "system": [agent group objects...], 311 - "apps": {"app_name": [agent group objects...], ...} 312 - }, 313 - "total_runs": int, 314 - "failed_runs": int 315 - } 316 - """ 317 - if not re.fullmatch(DATE_RE.pattern, day): 318 - return jsonify({"error": "Invalid day format"}), 400 319 - 320 - facet_filter = _get_facet_filter() 321 - 322 - # Load agent metadata for titles and grouping 323 - agents_meta = get_muse_configs(has_tools=True) 324 - facets = get_facets() 325 - 326 - # Get agents for this day 327 - agents = _get_agents_for_day(day, facet_filter) 328 - 329 - # Group by name 330 - name_groups = _group_agents_by_name(agents, agents_meta) 331 - 332 - # Organize into system vs app groups 333 - system_groups = [] 334 - app_groups: dict[str, list] = {} 335 - 336 - for name, group in name_groups.items(): 337 - # Add facet colors for display 338 - group["facet_colors"] = {} 339 - for facet_name in group["facets"]: 340 - if facet_name in facets: 341 - group["facet_colors"][facet_name] = facets[facet_name].get("color") 342 - 343 - if group["source"] == "system": 344 - system_groups.append(group) 345 - else: 346 - app_name = group["app"] or "unknown" 347 - if app_name not in app_groups: 348 - app_groups[app_name] = [] 349 - app_groups[app_name].append(group) 350 - 351 - # Sort groups by title 352 - system_groups.sort(key=lambda x: x["title"].lower()) 353 - for app_name in app_groups: 354 - app_groups[app_name].sort(key=lambda x: x["title"].lower()) 355 - 356 - # Calculate totals 357 - total_runs = sum(g["run_count"] for g in name_groups.values()) 358 - failed_runs = sum(g["failed_count"] for g in name_groups.values()) 359 - 360 - return jsonify( 361 - { 362 - "groups": { 363 - "system": system_groups, 364 - "apps": app_groups, 365 - }, 366 - "total_runs": total_runs, 367 - "failed_runs": failed_runs, 292 + "runs": [run objects...], 293 + "agents": {name: metadata...}, 294 + "facets": {name: {title, color}...} 368 295 } 369 - ) 370 - 371 - 372 - @agents_bp.route("/api/agents/<day>/<path:name>") 373 - def api_agent_runs(day: str, name: str) -> Any: 374 - """Get runs for a specific agent on a specific day. 375 - 376 - Returns list of runs with full details for display. 377 296 """ 378 297 if not re.fullmatch(DATE_RE.pattern, day): 379 298 return jsonify({"error": "Invalid day format"}), 400 380 299 381 300 facet_filter = _get_facet_filter() 382 301 383 - # Load metadata 384 - agents_meta = get_muse_configs(has_tools=True) 385 - facets = get_facets() 386 - 387 - # Get all agents for day and filter to this name 388 - all_agents = _get_agents_for_day(day, facet_filter) 389 - runs = [a for a in all_agents if a["name"] == name] 390 - 391 - # Add facet color to each run 392 - for run in runs: 393 - run_facet = run.get("facet") 394 - if run_facet and run_facet in facets: 395 - run["facet_color"] = facets[run_facet].get("color") 396 - run["facet_title"] = facets[run_facet].get("title", run_facet) 397 - 398 - # Get agent metadata 399 - meta = agents_meta.get(name, {}) 302 + runs = _get_agents_for_day(day, facet_filter) 303 + agents = _build_agents_meta() 304 + facets = { 305 + name: {"title": f.get("title", name), "color": f.get("color")} 306 + for name, f in get_facets().items() 307 + } 400 308 401 309 return jsonify( 402 310 { 403 - "name": name, 404 - "title": meta.get("title", name), 405 - "source": meta.get("source", "system"), 406 - "app": meta.get("app"), 407 311 "runs": runs, 408 - "run_count": len(runs), 409 - "failed_count": sum(1 for r in runs if r.get("failed")), 312 + "agents": agents, 313 + "facets": facets, 410 314 } 411 315 ) 412 316
+292 -67
apps/agents/workspace.html
··· 45 45 } 46 46 47 47 /* ============================================================================ 48 + Day Summary Bar 49 + ============================================================================ */ 50 + .day-summary { 51 + display: flex; 52 + gap: 1.5rem; 53 + padding: 0.5rem 0 1rem; 54 + font-size: 0.85rem; 55 + color: #666; 56 + } 57 + 58 + .day-summary .summary-item { 59 + display: flex; 60 + align-items: center; 61 + gap: 0.3rem; 62 + } 63 + 64 + .day-summary .summary-failed { 65 + color: #c62828; 66 + } 67 + 68 + /* ============================================================================ 48 69 Agent Card Grid 49 70 ============================================================================ */ 50 71 .agent-section { ··· 71 92 .agent-card { 72 93 background: white; 73 94 border: 2px solid #e0e0e0; 95 + border-left: 4px solid #6c757d; 74 96 border-radius: 8px; 75 97 padding: 1rem; 76 98 cursor: pointer; ··· 86 108 display: flex; 87 109 align-items: flex-start; 88 110 justify-content: space-between; 89 - margin-bottom: 0.5rem; 111 + margin-bottom: 0.25rem; 90 112 } 91 113 92 114 .agent-card-title { ··· 122 144 color: #c62828; 123 145 } 124 146 147 + .agent-card-description { 148 + font-size: 0.8rem; 149 + color: #888; 150 + margin: 0 0 0.5rem; 151 + overflow: hidden; 152 + text-overflow: ellipsis; 153 + white-space: nowrap; 154 + } 155 + 156 + .agent-card-caps { 157 + display: flex; 158 + gap: 0.4rem; 159 + margin-bottom: 0.5rem; 160 + flex-wrap: wrap; 161 + } 162 + 163 + .cap-badge { 164 + display: inline-flex; 165 + align-items: center; 166 + padding: 0.1rem 0.4rem; 167 + border-radius: 3px; 168 + font-size: 0.7rem; 169 + font-weight: 500; 170 + background: #f0f0f0; 171 + color: #777; 172 + letter-spacing: 0.02em; 173 + } 174 + 125 175 .agent-card-activity { 126 176 display: flex; 127 177 justify-content: space-between; 128 178 align-items: center; 129 179 gap: 0.75rem; 130 - margin-top: 0.5rem; 131 180 font-size: 0.8rem; 132 181 color: #666; 133 182 } ··· 223 272 224 273 .col-status { width: 50px; text-align: center; } 225 274 .col-time { width: 100px; } 275 + .col-model { width: 140px; } 226 276 .col-runtime { width: 80px; text-align: right; } 227 277 .col-activity { width: 50px; text-align: center; } 228 278 .col-facet { width: 120px; } ··· 255 305 font-size: 0.9rem; 256 306 } 257 307 308 + .model-name { 309 + font-size: 0.8rem; 310 + color: #888; 311 + } 312 + 313 + .error-preview { 314 + font-size: 0.75rem; 315 + color: #c62828; 316 + margin-top: 0.2rem; 317 + overflow: hidden; 318 + text-overflow: ellipsis; 319 + white-space: nowrap; 320 + max-width: 400px; 321 + } 322 + 258 323 /* ============================================================================ 259 324 Run Detail (Expanded Row) 260 325 ============================================================================ */ ··· 451 516 452 517 <!-- Card Grid View --> 453 518 <div id="grid-view" style="display: none;"> 519 + <div id="day-summary" class="day-summary" style="display: none;"></div> 454 520 <div id="agent-groups"></div> 455 521 <div id="empty-state" class="empty-state" style="display: none;"> 456 522 <div class="empty-state-icon">🤖</div> ··· 478 544 <tr> 479 545 <th class="col-status"></th> 480 546 <th class="col-time">Time</th> 547 + <th class="col-model">Model</th> 481 548 <th class="col-runtime">Runtime</th> 482 549 <th class="col-activity" title="Thinking events">💭</th> 483 550 <th class="col-activity" title="Tool calls">🔧</th> ··· 511 578 // State 512 579 let currentDay = null; 513 580 let currentName = null; 514 - let agentData = null; 581 + let allRuns = []; 582 + let agentsMeta = {}; 583 + let facetsMeta = {}; 515 584 let expandedRunId = null; 516 585 let runDetailCache = {}; 517 586 ··· 524 593 525 594 // Format timestamp to time string 526 595 function formatTime(ts) { 527 - if (!ts) return '—'; 596 + if (!ts) return '\u2014'; 528 597 const d = new Date(ts); 529 598 return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); 530 599 } 531 600 532 601 // Format runtime 533 602 function formatRuntime(seconds) { 534 - if (seconds === null || seconds === undefined) return '—'; 603 + if (seconds === null || seconds === undefined) return '\u2014'; 535 604 if (seconds < 60) return `${Math.round(seconds)}s`; 536 605 const mins = Math.floor(seconds / 60); 537 606 const secs = Math.round(seconds % 60); 538 607 return `${mins}m ${secs}s`; 539 608 } 540 609 541 - // Truncate prompt for display 542 - function truncatePrompt(prompt, maxLen = 80) { 543 - if (!prompt) return '—'; 544 - const clean = prompt.replace(/\s+/g, ' ').trim(); 610 + // Truncate text for display 611 + function truncate(text, maxLen = 80) { 612 + if (!text) return '\u2014'; 613 + const clean = text.replace(/\s+/g, ' ').trim(); 545 614 if (clean.length <= maxLen) return clean; 546 615 return clean.substring(0, maxLen) + '...'; 547 616 } 548 617 549 618 // Format cost in cents with exact USD in title 550 - // Returns {text: "3c", title: "$0.0312"} or {text: "—", title: ""} if null 551 619 function formatCost(costUSD) { 552 620 if (costUSD === null || costUSD === undefined) { 553 - return { text: '—', title: '' }; 621 + return { text: '\u2014', title: '' }; 554 622 } 555 623 const cents = Math.round(costUSD * 100); 556 624 const exactUSD = '$' + costUSD.toFixed(4); 557 625 return { text: cents + 'c', title: exactUSD }; 558 626 } 559 627 560 - // Load agents for current day 628 + // Shorten model name for display 629 + function formatModel(model) { 630 + if (!model) return '\u2014'; 631 + // Strip common provider prefixes and date suffixes 632 + return model 633 + .replace(/^(models\/|accounts\/[^/]+\/models\/)/, '') 634 + .replace(/-\d{8}$/, ''); 635 + } 636 + 637 + // ========================================================================= 638 + // Grouping & Aggregation (moved from server) 639 + // ========================================================================= 640 + 641 + function groupRunsByAgent(runs) { 642 + const groups = {}; 643 + 644 + for (const run of runs) { 645 + const name = run.name; 646 + if (!groups[name]) { 647 + const meta = agentsMeta[name] || {}; 648 + groups[name] = { 649 + name: name, 650 + title: meta.title || name, 651 + description: meta.description || null, 652 + color: meta.color || '#6c757d', 653 + source: meta.source || 'system', 654 + app: meta.app || null, 655 + schedule: meta.schedule || null, 656 + has_tools: meta.has_tools || false, 657 + has_output: meta.has_output || false, 658 + multi_facet: meta.multi_facet || false, 659 + run_count: 0, 660 + failed_count: 0, 661 + thinking_count: 0, 662 + tool_count: 0, 663 + total_cost: 0.0, 664 + facets: new Set(), 665 + }; 666 + } 667 + 668 + const g = groups[name]; 669 + g.run_count++; 670 + if (run.failed) g.failed_count++; 671 + g.thinking_count += (run.thinking_count || 0); 672 + g.tool_count += (run.tool_count || 0); 673 + if (run.cost != null) g.total_cost += run.cost; 674 + if (run.facet) g.facets.add(run.facet); 675 + } 676 + 677 + return groups; 678 + } 679 + 680 + function organizeGroups(groups) { 681 + const systemGroups = []; 682 + const appGroups = {}; 683 + 684 + for (const group of Object.values(groups)) { 685 + if (group.source === 'system') { 686 + systemGroups.push(group); 687 + } else { 688 + const appName = group.app || 'unknown'; 689 + if (!appGroups[appName]) appGroups[appName] = []; 690 + appGroups[appName].push(group); 691 + } 692 + } 693 + 694 + // Sort alphabetically by title 695 + const sortByTitle = (a, b) => a.title.toLowerCase().localeCompare(b.title.toLowerCase()); 696 + systemGroups.sort(sortByTitle); 697 + for (const appName of Object.keys(appGroups)) { 698 + appGroups[appName].sort(sortByTitle); 699 + } 700 + 701 + return { system: systemGroups, apps: appGroups }; 702 + } 703 + 704 + // ========================================================================= 705 + // Data Loading 706 + // ========================================================================= 707 + 561 708 async function loadAgents() { 562 709 currentDay = getDayFromUrl(); 563 710 if (!currentDay) return; ··· 568 715 569 716 try { 570 717 const response = await fetch(`api/agents/${currentDay}`); 571 - agentData = await response.json(); 718 + const data = await response.json(); 719 + 720 + allRuns = data.runs || []; 721 + agentsMeta = data.agents || {}; 722 + facetsMeta = data.facets || {}; 723 + 572 724 renderGridView(); 573 725 } catch (error) { 574 726 console.error('Error loading agents:', error); ··· 577 729 } 578 730 } 579 731 580 - // Render the card grid view 732 + // ========================================================================= 733 + // Grid View 734 + // ========================================================================= 735 + 581 736 function renderGridView() { 582 737 document.getElementById('loading-view').style.display = 'none'; 583 738 document.getElementById('grid-view').style.display = 'block'; ··· 586 741 const container = document.getElementById('agent-groups'); 587 742 container.innerHTML = ''; 588 743 589 - const { groups, total_runs } = agentData; 590 - const hasAgents = groups.system.length > 0 || Object.keys(groups.apps).length > 0; 591 - 592 - if (!hasAgents) { 744 + if (allRuns.length === 0) { 745 + document.getElementById('day-summary').style.display = 'none'; 593 746 document.getElementById('empty-state').style.display = 'block'; 594 747 return; 595 748 } 596 749 597 750 document.getElementById('empty-state').style.display = 'none'; 598 751 752 + // Group and organize 753 + const groups = groupRunsByAgent(allRuns); 754 + const organized = organizeGroups(groups); 755 + 756 + // Render summary bar 757 + renderDaySummary(allRuns); 758 + 599 759 // Render system agents 600 - if (groups.system.length > 0) { 601 - container.appendChild(renderSection('System Agents', groups.system)); 760 + if (organized.system.length > 0) { 761 + container.appendChild(renderSection('System Agents', organized.system)); 602 762 } 603 763 604 764 // Render app agent sections 605 - const appNames = Object.keys(groups.apps).sort(); 765 + const appNames = Object.keys(organized.apps).sort(); 606 766 for (const appName of appNames) { 607 767 const title = appName.charAt(0).toUpperCase() + appName.slice(1); 608 - container.appendChild(renderSection(title, groups.apps[appName])); 768 + container.appendChild(renderSection(title, organized.apps[appName])); 609 769 } 610 770 } 611 771 772 + function renderDaySummary(runs) { 773 + const summary = document.getElementById('day-summary'); 774 + const totalRuns = runs.length; 775 + const failedRuns = runs.filter(r => r.failed).length; 776 + let totalCost = 0; 777 + for (const r of runs) { 778 + if (r.cost != null) totalCost += r.cost; 779 + } 780 + 781 + const parts = []; 782 + parts.push(`<span class="summary-item">${totalRuns} run${totalRuns !== 1 ? 's' : ''}</span>`); 783 + if (failedRuns > 0) { 784 + parts.push(`<span class="summary-item summary-failed">${failedRuns} failed</span>`); 785 + } 786 + if (totalCost > 0) { 787 + const costInfo = formatCost(totalCost); 788 + parts.push(`<span class="summary-item" title="${costInfo.title}">💰 ${costInfo.text}</span>`); 789 + } 790 + 791 + summary.innerHTML = parts.join(''); 792 + summary.style.display = totalRuns > 0 ? 'flex' : 'none'; 793 + } 794 + 612 795 // Render a section with cards 613 796 function renderSection(title, agents) { 614 797 const section = document.createElement('div'); ··· 634 817 function renderCard(agent) { 635 818 const card = document.createElement('div'); 636 819 card.className = 'agent-card'; 820 + card.style.borderLeftColor = agent.color; 637 821 card.onclick = () => showRunList(agent.name); 638 822 639 823 // Header with title and badges ··· 648 832 const badges = document.createElement('div'); 649 833 badges.className = 'agent-card-badges'; 650 834 651 - // Success count badge (green) 652 835 const successCount = agent.run_count - agent.failed_count; 653 836 if (successCount > 0) { 654 837 const successBadge = document.createElement('span'); ··· 657 840 badges.appendChild(successBadge); 658 841 } 659 842 660 - // Failed count badge (red) 661 843 if (agent.failed_count > 0) { 662 844 const failBadge = document.createElement('span'); 663 845 failBadge.className = 'badge badge-failed'; ··· 668 850 header.appendChild(badges); 669 851 card.appendChild(header); 670 852 671 - // Activity row: counts on left, facet dots on right (in all-facet mode) 853 + // Description 854 + if (agent.description) { 855 + const desc = document.createElement('p'); 856 + desc.className = 'agent-card-description'; 857 + desc.textContent = agent.description; 858 + desc.title = agent.description; 859 + card.appendChild(desc); 860 + } 861 + 862 + // Capability badges 863 + const caps = []; 864 + if (agent.schedule) caps.push(agent.schedule); 865 + if (agent.has_tools) caps.push('tools'); 866 + if (agent.has_output && !agent.has_tools) caps.push('output'); 867 + if (agent.multi_facet) caps.push('per-facet'); 868 + 869 + if (caps.length > 0) { 870 + const capsRow = document.createElement('div'); 871 + capsRow.className = 'agent-card-caps'; 872 + for (const cap of caps) { 873 + const pill = document.createElement('span'); 874 + pill.className = 'cap-badge'; 875 + pill.textContent = cap; 876 + capsRow.appendChild(pill); 877 + } 878 + card.appendChild(capsRow); 879 + } 880 + 881 + // Activity row: counts on left, facet dots on right 672 882 const hasActivity = agent.thinking_count > 0 || agent.tool_count > 0 || agent.total_cost > 0; 673 - const hasFacetDots = agent.facet_colors && Object.keys(agent.facet_colors).length > 0; 883 + const facetNames = Array.from(agent.facets); 884 + const hasFacetDots = facetNames.length > 0; 674 885 675 886 if (hasActivity || hasFacetDots) { 676 887 const activity = document.createElement('div'); 677 888 activity.className = 'agent-card-activity'; 678 889 679 - // Activity counts container (left side) 680 890 const counts = document.createElement('div'); 681 891 counts.className = 'activity-counts'; 682 892 ··· 707 917 708 918 activity.appendChild(counts); 709 919 710 - // Facet dots (right side, all-facet mode only) 711 920 if (hasFacetDots) { 712 921 const dots = document.createElement('div'); 713 922 dots.className = 'facet-dots'; 714 923 715 - for (const [facetName, color] of Object.entries(agent.facet_colors)) { 924 + for (const facetName of facetNames) { 925 + const facet = facetsMeta[facetName]; 716 926 const dot = document.createElement('span'); 717 927 dot.className = 'facet-dot'; 718 - dot.style.backgroundColor = color || '#999'; 719 - dot.title = facetName; 928 + dot.style.backgroundColor = (facet && facet.color) || '#999'; 929 + dot.title = (facet && facet.title) || facetName; 720 930 dots.appendChild(dot); 721 931 } 722 932 ··· 729 939 return card; 730 940 } 731 941 732 - // Show run list for a specific agent 733 - async function showRunList(name) { 942 + // ========================================================================= 943 + // Run List View 944 + // ========================================================================= 945 + 946 + function showRunList(name) { 734 947 currentName = name; 735 948 expandedRunId = null; 736 949 runDetailCache = {}; ··· 738 951 document.getElementById('grid-view').style.display = 'none'; 739 952 document.getElementById('list-view').style.display = 'block'; 740 953 741 - const tbody = document.getElementById('runs-tbody'); 742 - tbody.innerHTML = '<tr><td colspan="8" class="run-loading"><div class="spinner"></div></td></tr>'; 954 + // Filter runs client-side 955 + const runs = allRuns.filter(r => r.name === name); 956 + const meta = agentsMeta[name] || {}; 957 + const title = meta.title || name; 958 + const failedCount = runs.filter(r => r.failed).length; 743 959 744 - try { 745 - const response = await fetch(`api/agents/${currentDay}/${encodeURIComponent(name)}`); 746 - const data = await response.json(); 960 + document.getElementById('list-view-title').textContent = title; 961 + document.getElementById('list-view-count').textContent = 962 + `${runs.length} run${runs.length !== 1 ? 's' : ''}` + 963 + (failedCount > 0 ? ` (${failedCount} failed)` : ''); 747 964 748 - document.getElementById('list-view-title').textContent = data.title; 749 - document.getElementById('list-view-count').textContent = 750 - `${data.run_count} run${data.run_count !== 1 ? 's' : ''}` + 751 - (data.failed_count > 0 ? ` (${data.failed_count} failed)` : ''); 752 - 753 - renderRunList(data.runs); 754 - } catch (error) { 755 - console.error('Error loading runs:', error); 756 - tbody.innerHTML = '<tr><td colspan="8" class="run-loading">Error loading runs</td></tr>'; 757 - } 965 + renderRunList(runs); 758 966 } 759 967 760 968 // Render the run list table ··· 763 971 tbody.innerHTML = ''; 764 972 765 973 if (runs.length === 0) { 766 - tbody.innerHTML = '<tr><td colspan="8" class="run-loading">No runs found</td></tr>'; 974 + tbody.innerHTML = '<tr><td colspan="9" class="run-loading">No runs found</td></tr>'; 767 975 return; 768 976 } 769 977 770 978 for (const run of runs) { 771 - // Main row 772 979 const row = document.createElement('tr'); 773 980 row.dataset.runId = run.id; 774 981 row.onclick = () => toggleRunDetail(run.id); ··· 780 987 statusIcon.className = 'status-icon'; 781 988 if (run.status === 'running') { 782 989 statusIcon.className += ' status-running'; 783 - statusIcon.textContent = '⏳'; 990 + statusIcon.textContent = '\u23f3'; 784 991 statusIcon.title = 'Running'; 785 992 } else if (run.failed) { 786 993 statusIcon.className += ' status-failed'; 787 - statusIcon.textContent = '✗'; 994 + statusIcon.textContent = '\u2717'; 788 995 statusIcon.title = 'Failed'; 789 996 } else { 790 997 statusIcon.className += ' status-ok'; 791 - statusIcon.textContent = '✓'; 998 + statusIcon.textContent = '\u2713'; 792 999 statusIcon.title = 'Completed'; 793 1000 } 794 1001 statusCell.appendChild(statusIcon); ··· 800 1007 timeCell.textContent = formatTime(run.start); 801 1008 row.appendChild(timeCell); 802 1009 1010 + // Model 1011 + const modelCell = document.createElement('td'); 1012 + modelCell.className = 'col-model'; 1013 + const modelSpan = document.createElement('span'); 1014 + modelSpan.className = 'model-name'; 1015 + modelSpan.textContent = formatModel(run.model); 1016 + modelSpan.title = run.model || ''; 1017 + modelCell.appendChild(modelSpan); 1018 + row.appendChild(modelCell); 1019 + 803 1020 // Runtime 804 1021 const runtimeCell = document.createElement('td'); 805 1022 runtimeCell.className = 'col-runtime'; ··· 830 1047 const facetCell = document.createElement('td'); 831 1048 facetCell.className = 'col-facet'; 832 1049 if (run.facet) { 1050 + const facet = facetsMeta[run.facet]; 833 1051 const tag = document.createElement('span'); 834 1052 tag.className = 'facet-tag'; 835 - if (run.facet_color) { 1053 + if (facet && facet.color) { 836 1054 const dot = document.createElement('span'); 837 1055 dot.className = 'facet-dot'; 838 - dot.style.backgroundColor = run.facet_color; 1056 + dot.style.backgroundColor = facet.color; 839 1057 tag.appendChild(dot); 840 1058 } 841 - tag.appendChild(document.createTextNode(run.facet_title || run.facet)); 1059 + tag.appendChild(document.createTextNode((facet && facet.title) || run.facet)); 842 1060 facetCell.appendChild(tag); 843 1061 } else { 844 - facetCell.textContent = '—'; 1062 + facetCell.textContent = '\u2014'; 845 1063 } 846 1064 row.appendChild(facetCell); 847 1065 848 - // Prompt 1066 + // Prompt + error preview 849 1067 const promptCell = document.createElement('td'); 850 1068 promptCell.className = 'col-prompt'; 851 1069 const promptSpan = document.createElement('span'); 852 1070 promptSpan.className = 'prompt-snippet'; 853 - promptSpan.textContent = truncatePrompt(run.prompt); 854 - promptSpan.title = run.prompt; 1071 + promptSpan.textContent = truncate(run.prompt); 1072 + promptSpan.title = run.prompt || ''; 855 1073 promptCell.appendChild(promptSpan); 1074 + 1075 + if (run.failed && run.error_message) { 1076 + const errorDiv = document.createElement('div'); 1077 + errorDiv.className = 'error-preview'; 1078 + errorDiv.textContent = run.error_message; 1079 + errorDiv.title = run.error_message; 1080 + promptCell.appendChild(errorDiv); 1081 + } 1082 + 856 1083 row.appendChild(promptCell); 857 1084 858 1085 tbody.appendChild(row); ··· 888 1115 detailRow.dataset.runId = runId; 889 1116 890 1117 const detailCell = document.createElement('td'); 891 - detailCell.colSpan = 8; 1118 + detailCell.colSpan = 9; 892 1119 893 1120 const content = document.createElement('div'); 894 1121 content.className = 'run-detail-content'; 895 1122 896 - // Check cache - cache stores {html, thinking_count, tool_count, cost} 897 1123 if (runDetailCache[runId]) { 898 1124 const cached = runDetailCache[runId]; 899 1125 content.innerHTML = buildDetailContent(cached.html, cached.thinking_count, cached.tool_count, cached.cost); 900 1126 } else { 901 1127 content.innerHTML = '<div class="run-loading"><div class="spinner"></div><div>Loading...</div></div>'; 902 1128 903 - // Fetch run detail 904 1129 try { 905 1130 const response = await fetch(`api/run/${runId}`); 906 1131 const data = await response.json(); ··· 908 1133 if (data.error) { 909 1134 content.innerHTML = `<div class="run-loading">Error: ${data.error}</div>`; 910 1135 } else { 911 - // Convert markdown to HTML using marked library 912 1136 const html = marked.parse(data.markdown || '', { breaks: true, gfm: true }); 913 1137 runDetailCache[runId] = { 914 1138 html: html, ··· 950 1174 return `${headerHtml}<div class="markdown">${html}</div>`; 951 1175 } 952 1176 953 - // Show grid view 1177 + // ========================================================================= 1178 + // Navigation & Modals 1179 + // ========================================================================= 1180 + 954 1181 window.showGridView = function() { 955 1182 currentName = null; 956 1183 expandedRunId = null; ··· 958 1185 document.getElementById('list-view').style.display = 'none'; 959 1186 }; 960 1187 961 - // Show preview modal 962 1188 window.showPreview = async function() { 963 1189 if (!currentName) return; 964 1190 ··· 984 1210 } 985 1211 }; 986 1212 987 - // Hide preview modal 988 1213 window.hidePreview = function() { 989 1214 document.getElementById('preview-modal').classList.remove('show'); 990 1215 };