personal memory agent
0
fork

Configure Feed

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

agents: add output file column and viewer modal to run list

+176 -3
+57 -1
apps/agents/routes.py
··· 18 18 from convey.utils import DATE_RE, format_date 19 19 from think.facets import get_facets 20 20 from think.models import calc_agent_cost 21 - from think.muse import get_muse_configs 21 + from think.muse import get_muse_configs, get_output_path 22 22 23 23 agents_bp = Blueprint( 24 24 "app:agents", ··· 148 148 "error_message": event_data["error_message"], 149 149 } 150 150 151 + # Check for output file (generators only) 152 + output_file = None 153 + req_output = request_event.get("output") 154 + if req_output: 155 + req_day = request_event.get("day") 156 + req_segment = request_event.get("segment") 157 + req_facet = request_event.get("facet") 158 + req_name = request_event.get("name", "default") 159 + if req_day: 160 + day_dir = Path(state.journal_root) / req_day 161 + out_path = get_output_path( 162 + day_dir, 163 + req_name, 164 + segment=req_segment, 165 + output_format=req_output, 166 + facet=req_facet, 167 + ) 168 + if out_path.exists(): 169 + # Relative to day dir: "agents/activity.md" or "120000_1800/media.md" 170 + output_file = str(out_path.relative_to(day_dir)) 171 + agent_info["output_file"] = output_file 172 + 151 173 # For completed agents, determine end state and calculate cost 152 174 if not is_active: 153 175 end_state = get_agent_end_state(agent_id) ··· 231 253 "schedule": config.get("schedule"), 232 254 "has_tools": "tools" in config, 233 255 "has_output": "output" in config, 256 + "output_format": config.get("output"), 234 257 "multi_facet": bool(config.get("multi_facet")), 235 258 } 236 259 ··· 342 365 ) 343 366 except Exception as e: 344 367 return jsonify({"error": str(e)}), 500 368 + 369 + 370 + @agents_bp.route("/api/output/<day>/<path:filename>") 371 + def api_output_file(day: str, filename: str) -> Any: 372 + """Serve output file content for the viewer modal. 373 + 374 + Returns JSON with content, format, and filename. 375 + Path is validated to stay within the day directory. 376 + """ 377 + if not DATE_RE.fullmatch(day): 378 + return jsonify(error="Invalid day format"), 400 379 + 380 + day_dir = Path(state.journal_root) / day 381 + file_path = (day_dir / filename).resolve() 382 + 383 + # Security: ensure path is within the day directory 384 + try: 385 + file_path.relative_to(day_dir.resolve()) 386 + except ValueError: 387 + return jsonify(error="Invalid path"), 403 388 + 389 + if not file_path.is_file(): 390 + return jsonify(error="File not found"), 404 391 + 392 + ext = file_path.suffix.lower() 393 + fmt = "json" if ext == ".json" else "md" 394 + 395 + try: 396 + content = file_path.read_text(encoding="utf-8") 397 + except IOError: 398 + return jsonify(error="Could not read file"), 500 399 + 400 + return jsonify(content=content, format=fmt, filename=file_path.name) 345 401 346 402 347 403 @agents_bp.route("/api/preview/<path:name>")
+119 -2
apps/agents/workspace.html
··· 276 276 .col-runtime { width: 80px; text-align: right; } 277 277 .col-activity { width: 50px; text-align: center; } 278 278 .col-facet { width: 120px; } 279 + .col-output { width: 120px; } 279 280 .col-prompt { } 281 + 282 + .output-link { 283 + color: #1a73e8; 284 + text-decoration: none; 285 + font-size: 0.85rem; 286 + cursor: pointer; 287 + } 288 + .output-link:hover { 289 + text-decoration: underline; 290 + } 280 291 281 292 .status-icon { 282 293 font-size: 1.1rem; ··· 631 642 margin: 0; 632 643 } 633 644 645 + /* Output viewer modal - larger than preview modal */ 646 + #output-modal .modal-content { 647 + max-width: 1000px; 648 + max-height: 90vh; 649 + } 650 + 651 + #output-modal .modal-body .rendered-markdown { 652 + line-height: 1.6; 653 + font-size: 0.9rem; 654 + } 655 + 656 + #output-modal .modal-body .rendered-markdown h1, 657 + #output-modal .modal-body .rendered-markdown h2, 658 + #output-modal .modal-body .rendered-markdown h3 { 659 + margin-top: 1rem; 660 + margin-bottom: 0.5rem; 661 + } 662 + 663 + #output-modal .modal-body .rendered-markdown ul, 664 + #output-modal .modal-body .rendered-markdown ol { 665 + padding-left: 1.5rem; 666 + } 667 + 668 + #output-modal .modal-body .rendered-markdown p { 669 + margin-bottom: 0.5rem; 670 + } 671 + 634 672 /* ============================================================================ 635 673 Empty States 636 674 ============================================================================ */ ··· 724 762 <th class="col-activity" title="Tool calls">🔧</th> 725 763 <th class="col-activity" title="Cost">💰</th> 726 764 <th class="col-facet">Facet</th> 765 + <th class="col-output">Output</th> 727 766 <th class="col-prompt">Prompt</th> 728 767 </tr> 729 768 </thead> ··· 743 782 </div> 744 783 <div class="modal-body"> 745 784 <pre id="preview-modal-content"></pre> 785 + </div> 786 + </div> 787 + </div> 788 + 789 + <!-- Output Viewer Modal --> 790 + <div id="output-modal" class="modal-backdrop"> 791 + <div class="modal-content"> 792 + <div class="modal-header"> 793 + <h3 class="modal-title" id="output-modal-title">Output</h3> 794 + <button class="modal-close" onclick="hideOutputModal()">&times;</button> 795 + </div> 796 + <div class="modal-body" id="output-modal-body"> 746 797 </div> 747 798 </div> 748 799 </div> ··· 1143 1194 tbody.innerHTML = ''; 1144 1195 1145 1196 if (runs.length === 0) { 1146 - tbody.innerHTML = '<tr><td colspan="9" class="run-loading">No runs found</td></tr>'; 1197 + tbody.innerHTML = '<tr><td colspan="10" class="run-loading">No runs found</td></tr>'; 1147 1198 return; 1148 1199 } 1149 1200 ··· 1235 1286 } 1236 1287 row.appendChild(facetCell); 1237 1288 1289 + // Output file 1290 + const outputCell = document.createElement('td'); 1291 + outputCell.className = 'col-output'; 1292 + if (run.output_file) { 1293 + const link = document.createElement('a'); 1294 + link.className = 'output-link'; 1295 + link.textContent = run.output_file.split('/').pop(); 1296 + link.title = run.output_file; 1297 + link.onclick = function(e) { 1298 + e.stopPropagation(); 1299 + showOutputFile(currentDay, run.output_file); 1300 + }; 1301 + outputCell.appendChild(link); 1302 + } else { 1303 + outputCell.textContent = '\u2014'; 1304 + } 1305 + row.appendChild(outputCell); 1306 + 1238 1307 // Prompt + error preview 1239 1308 const promptCell = document.createElement('td'); 1240 1309 promptCell.className = 'col-prompt'; ··· 1287 1356 detailRow.dataset.runId = runId; 1288 1357 1289 1358 const detailCell = document.createElement('td'); 1290 - detailCell.colSpan = 9; 1359 + detailCell.colSpan = 10; 1291 1360 1292 1361 const content = document.createElement('div'); 1293 1362 content.className = 'run-detail-content'; ··· 1607 1676 document.addEventListener('keydown', function(e) { 1608 1677 if (e.key === 'Escape') { 1609 1678 hidePreview(); 1679 + hideOutputModal(); 1680 + } 1681 + }); 1682 + 1683 + // Output viewer modal 1684 + window.showOutputFile = async function(day, filename) { 1685 + const modal = document.getElementById('output-modal'); 1686 + const body = document.getElementById('output-modal-body'); 1687 + const title = document.getElementById('output-modal-title'); 1688 + 1689 + body.innerHTML = '<div class="run-loading"><div class="spinner"></div><div>Loading...</div></div>'; 1690 + title.textContent = filename.split('/').pop(); 1691 + modal.classList.add('show'); 1692 + 1693 + try { 1694 + const response = await fetch(`api/output/${day}/${filename}`); 1695 + const data = await response.json(); 1696 + 1697 + if (data.error) { 1698 + body.innerHTML = `<div class="run-loading">Error: ${data.error}</div>`; 1699 + } else if (data.format === 'json') { 1700 + const pre = document.createElement('pre'); 1701 + try { 1702 + pre.textContent = JSON.stringify(JSON.parse(data.content), null, 2); 1703 + } catch (e) { 1704 + pre.textContent = data.content; 1705 + } 1706 + body.innerHTML = ''; 1707 + body.appendChild(pre); 1708 + } else { 1709 + const div = document.createElement('div'); 1710 + div.className = 'rendered-markdown'; 1711 + div.innerHTML = marked.parse(data.content, { breaks: true, gfm: true }); 1712 + body.innerHTML = ''; 1713 + body.appendChild(div); 1714 + } 1715 + } catch (error) { 1716 + body.innerHTML = '<div class="run-loading">Error loading output file</div>'; 1717 + } 1718 + }; 1719 + 1720 + window.hideOutputModal = function() { 1721 + document.getElementById('output-modal').classList.remove('show'); 1722 + }; 1723 + 1724 + document.getElementById('output-modal').addEventListener('click', function(e) { 1725 + if (e.target === this) { 1726 + hideOutputModal(); 1610 1727 } 1611 1728 }); 1612 1729