@@ -15,6 +15,7 @@ let showStoppedJobs = false;
1515let showOrphanedOnly = false ;
1616let jobsLimit = 20 ;
1717let jobsFilterText = '' ;
18+ let lastJobsHash = '' ; // Cache hash to prevent unnecessary table rebuilds
1819
1920// Initialize on page load
2021document . addEventListener ( 'DOMContentLoaded' , async ( ) => {
@@ -800,16 +801,22 @@ function createExpandedRow(exp, numFlagColumns) {
800801 <div style="display: flex; gap: 20px; align-items: flex-start; margin-bottom: 20px;">
801802 <div class="detail-section" style="width: auto; min-width: 400px;">
802803 <div style="display: flex; justify-content: space-between; align-items: center;">
803- <h3>Configuration</h3>
804- <div id="config-buttons-${ exp . id } ">
805- <button class="btn btn-small" onclick="toggleConfigEdit('${ exp . id } ')" id="config-edit-btn-${ exp . id } ">Edit</button>
806- </div>
804+ <h3 style="display: flex; align-items: center; gap: 8px;">
805+ Configuration
806+ <span id="config-buttons-${ exp . id } ">
807+ ${ editingRows . has ( exp . id ) ? `
808+ <a onclick="toggleConfigEdit('${ exp . id } ')" id="config-edit-btn-${ exp . id } " class="wandb-link" style="cursor: pointer; margin-left: 0; margin-right: 6px; font-size: 12px; color: #4caf50;" title="Save changes">✓</a>
809+ <a onclick="cancelConfigEdit('${ exp . id } ')" class="wandb-link" style="cursor: pointer; margin-left: 0; font-size: 12px; color: #f44336;" title="Cancel">✗</a>
810+ ` : `
811+ <a onclick="toggleConfigEdit('${ exp . id } ')" id="config-edit-btn-${ exp . id } " class="wandb-link" style="cursor: pointer; margin-left: 0; font-size: 11px;" title="Edit configuration">✎</a>
812+ ` }
813+ </span>
814+ </h3>
807815 </div>
808816 <div class="detail-grid" id="config-grid-${ exp . id } ">
809817 <span class="detail-label">Experiment ID:</span>
810818 <span class="detail-value">
811819 <span class="config-field" data-field="id">${ exp . id } </span>
812- <button class="copy-btn" onclick="event.stopPropagation(); copyToClipboard('${ exp . id . replace ( / ' / g, "\\'" ) } ', event); return false;" title="Copy experiment ID">⎘</button>
813820 <a href="https://wandb.ai/metta-research/metta/runs/${ exp . id } " target="_blank" class="wandb-link" onclick="event.stopPropagation();" title="Open in W&B">W&B</a>
814821 <a href="https://app.datadoghq.com/logs?query=metta_run_id%3A%22${ exp . id } %22" target="_blank" class="wandb-link" onclick="event.stopPropagation();" title="Open in Datadog">log</a>
815822 </span>
@@ -838,7 +845,7 @@ function createExpandedRow(exp, numFlagColumns) {
838845 </div>
839846 </div>
840847
841- <div class="detail-section" style="width: 200px ; flex-shrink: 0;">
848+ <div class="detail-section" style="width: auto ; flex-shrink: 0;">
842849 <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
843850 <h3 style="margin: 0;">Jobs</h3>
844851 <div style="display: flex; gap: 5px;">
@@ -863,10 +870,10 @@ function createExpandedRow(exp, numFlagColumns) {
863870 <!-- Second Row: Command Panel -->
864871 <div style="display: flex;">
865872 <div class="detail-section" style="flex: 1;">
866- <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
867- <h3 style="margin: 0;"> Command</h3>
868- <button class="copy-btn" onclick="event.stopPropagation(); copyToClipboard('${ escapedCommand } ', 'Command copied!'); return false;" title="Copy command" style="padding: 4px 10px; font-size: 12px;">⎘ Copy </button>
869- </div >
873+ <h3 style="margin: 0 0 8px 0 ;">
874+ Command
875+ <button class="copy-btn copy-btn-left " onclick="event.stopPropagation(); copyToClipboard('${ escapedCommand } ', 'Command copied!'); return false;" title="Copy command">⎘ </button>
876+ </h3 >
870877 <div style="background-color: #f5f5f5; padding: 10px; border-radius: 4px; font-family: 'Monaco', 'Menlo', 'Courier New', monospace; font-size: 12px; overflow-x: auto; text-align: left; white-space: pre-wrap; word-break: break-word;">${ fullCommand } </div>
871878 </div>
872879 </div>
@@ -890,11 +897,19 @@ function buildCommand(exp) {
890897 parts . push ( `--gpus=${ exp . gpus } ` ) ;
891898 parts . push ( `--nodes=${ exp . nodes } ` ) ;
892899
900+ // Add git branch if specified (skip invalid values like "-")
901+ if ( exp . git_branch && exp . git_branch !== '-' ) {
902+ parts . push ( `--git-ref=${ exp . git_branch } ` ) ;
903+ }
904+
893905 // Add tool path
894906 if ( exp . tool_path ) {
895907 parts . push ( exp . tool_path ) ;
896908 }
897909
910+ // Always add run name using experiment ID
911+ parts . push ( `run=${ exp . id } ` ) ;
912+
898913 // Add all flags in sorted order
899914 const sortedFlags = Object . entries ( exp . flags || { } ) . sort ( ( a , b ) => a [ 0 ] . localeCompare ( b [ 0 ] ) ) ;
900915 for ( const [ key , value ] of sortedFlags ) {
@@ -1007,24 +1022,34 @@ async function loadJobHistory(experimentId) {
10071022 if ( startBtn ) startBtn . disabled = hasRunningJob ;
10081023 if ( stopBtn ) stopBtn . disabled = ! hasRunningJob ;
10091024
1010- if ( jobs . length === 0 ) {
1011- container . innerHTML = '<p style="color: #999; font-size: 12px;">No jobs yet</p>' ;
1012- return ;
1013- }
1014-
1015- // Show all jobs in order
1016- const html = jobs . map ( job => {
1025+ // Show all jobs in table format
1026+ const rows = jobs . map ( job => {
1027+ // Extract numeric ID from full ID (e.g., "daveey.ca4.4x4.mcl_1.0-9482" -> "9482")
1028+ const numericId = job . id . split ( '-' ) . pop ( ) ;
10171029 return `
1018- <div class="job-item" style="display: flex; align-items: center; gap: 8px; padding: 4px 0; font-size: 12px;">
1019- <span style="min-width: 45px; font-weight: 500;">#${ job . id } </span>
1020- <a href="https://skypilot-api.softmax-research.net/dashboard/jobs/${ job . id } " target="_blank" class="wandb-link" onclick="event.stopPropagation();" title="Open in SkyPilot Dashboard" style="font-size: 10px;">sky</a>
1021- <a href="https://app.datadoghq.com/logs?query=metta_run_id%3A%22${ job . experiment_id } %22" target="_blank" class="wandb-link" onclick="event.stopPropagation();" title="Open in Datadog" style="font-size: 10px;">log</a>
1022- <span class="status-badge ${ job . status . toLowerCase ( ) } " title="${ job . status } ">${ abbreviateStatus ( job . status ) } </span>
1023- <span style="color: #666; font-size: 11px;">${ formatTime ( job . created_at ) } </span>
1024- </div>
1025- ` ;
1030+ <tr>
1031+ <td style="padding: 4px 6px; font-size: 11px; border-bottom: 1px solid #eee; white-space: nowrap; cursor: pointer;" onclick="event.stopPropagation(); copyToClipboard('${ job . id } ', event); return false;" title="Click to copy full ID">${ numericId } </td>
1032+ <td style="padding: 4px 6px; font-size: 11px; border-bottom: 1px solid #eee; white-space: nowrap;"><span class="status-badge ${ job . status . toLowerCase ( ) } " title="${ job . status } ">${ abbreviateStatus ( job . status ) } </span></td>
1033+ <td style="padding: 4px 6px; font-size: 11px; border-bottom: 1px solid #eee; white-space: nowrap;"><a href="https://app.datadoghq.com/logs?query=metta_run_id%3A%22${ job . experiment_id } %22" target="_blank" class="wandb-link" onclick="event.stopPropagation();" title="Open in Datadog">log</a></td>
1034+ </tr>
1035+ ` ;
10261036 } ) . join ( '' ) ;
10271037
1038+ const html = `
1039+ <table style="width: 100%; border-collapse: collapse; font-size: 11px;">
1040+ <thead>
1041+ <tr style="background: #f5f5f5;">
1042+ <th style="padding: 4px 6px; text-align: left; font-weight: 600; border-bottom: 2px solid #e0e0e0; font-size: 10px; white-space: nowrap;">Job ID</th>
1043+ <th style="padding: 4px 6px; text-align: left; font-weight: 600; border-bottom: 2px solid #e0e0e0; font-size: 10px; white-space: nowrap;">Status</th>
1044+ <th style="padding: 4px 6px; text-align: left; font-weight: 600; border-bottom: 2px solid #e0e0e0; font-size: 10px; white-space: nowrap;">Links</th>
1045+ </tr>
1046+ </thead>
1047+ <tbody>
1048+ ${ rows }
1049+ </tbody>
1050+ </table>
1051+ ` ;
1052+
10281053 container . innerHTML = html ;
10291054 } catch ( error ) {
10301055 container . innerHTML = '<p style="color: #f44336; font-size: 12px;">Error loading jobs</p>' ;
@@ -1071,7 +1096,7 @@ async function loadCheckpoints(experimentId) {
10711096 return `
10721097 <tr>
10731098 <td style="padding: 4px 6px; font-size: 11px; border-bottom: 1px solid #eee; white-space: nowrap;">${ cp . epoch } </td>
1074- <td style="padding: 4px 6px; font-size: 11px; border-bottom: 1px solid #eee; color: #666; white-space: nowrap;">${ formatTime ( cp . created_at ) } </td>
1099+ <td style="padding: 4px 6px; font-size: 11px; border-bottom: 1px solid #eee; color: #666; white-space: nowrap;">${ formatDuration ( cp . created_at ) } </td>
10751100 <td style="padding: 4px 6px; font-size: 11px; border-bottom: 1px solid #eee; white-space: nowrap;">${ s3Pill } ${ mettaPill } ${ obsPill } </td>
10761101 <td style="padding: 4px 6px; font-size: 11px; border-bottom: 1px solid #eee; text-align: center; white-space: nowrap;">${ replayCount } </td>
10771102 </tr>
@@ -1173,20 +1198,20 @@ async function toggleConfigEdit(experimentId) {
11731198 const flagsTable = document . getElementById ( `flags-table-${ experimentId } ` ) ;
11741199 const addFlagBtn = document . getElementById ( `add-flag-btn-${ experimentId } ` ) ;
11751200
1176- if ( btn . textContent === 'Edit ' ) {
1201+ if ( btn . textContent === '✎ ' ) {
11771202 // Enter edit mode
1178- btn . textContent = 'Save' ;
1179- // Add Cancel button
1203+ // Add Save and Cancel icons
11801204 btnContainer . innerHTML = `
1181- <button class="btn btn-small" onclick="toggleConfigEdit('${ experimentId } ')" id="config-edit-btn-${ experimentId } "> Save</button >
1182- <button class="btn btn-small" onclick="cancelConfigEdit('${ experimentId } ')"> Cancel</button >
1205+ <a onclick="toggleConfigEdit('${ experimentId } ')" id="config-edit-btn-${ experimentId } " class="wandb-link" style="cursor: pointer; margin-left: 0; margin-right: 6px; font-size: 12px; color: #4caf50;" title=" Save changes">✓</a >
1206+ <a onclick="cancelConfigEdit('${ experimentId } ')" class="wandb-link" style="cursor: pointer; margin-left: 0; font-size: 12px; color: #f44336;" title=" Cancel">✗</a >
11831207 ` ;
11841208 editingRows . add ( experimentId ) ; // Track that this row is being edited
11851209
11861210 // Make config fields editable
11871211 fields . forEach ( field => {
11881212 const currentValue = field . textContent === '-' ? '' : field . textContent ;
11891213 field . dataset . originalValue = currentValue ;
1214+ field . textContent = currentValue ; // Clear '-' from display
11901215
11911216 field . contentEditable = 'true' ;
11921217 field . style . backgroundColor = '#fff8e1' ;
@@ -1225,7 +1250,7 @@ async function toggleConfigEdit(experimentId) {
12251250
12261251 } else {
12271252 // Save mode
1228- btnContainer . innerHTML = `<button class="btn btn-small" onclick="toggleConfigEdit(' ${ experimentId } ')" id="config-edit-btn- ${ experimentId } " disabled> Saving...</button >` ;
1253+ btnContainer . innerHTML = `<a class="wandb-link" style="cursor: not-allowed; margin-left: 0; font-size: 12px; color: #999;" title=" Saving...">✓</a >` ;
12291254 editingRows . delete ( experimentId ) ; // Remove from editing tracking
12301255
12311256 try {
@@ -1376,6 +1401,22 @@ function updateJobsTable(jobs) {
13761401 ) ;
13771402 }
13781403
1404+ // Compute hash to detect changes
1405+ const jobsHash = JSON . stringify ( filteredJobs . map ( j => ( {
1406+ id : j . id ,
1407+ status : j . status ,
1408+ experiment_id : j . experiment_id ,
1409+ nodes : j . nodes ,
1410+ gpus : j . gpus ,
1411+ started_at : j . started_at
1412+ } ) ) ) + selectedJobs . size ;
1413+
1414+ // Only update if data has changed
1415+ if ( jobsHash === lastJobsHash && tbody . children . length > 0 ) {
1416+ return ;
1417+ }
1418+ lastJobsHash = jobsHash ;
1419+
13791420 if ( ! filteredJobs || filteredJobs . length === 0 ) {
13801421 tbody . innerHTML = '<tr><td colspan="7" class="empty-state">No jobs match the current filters</td></tr>' ;
13811422 updateJobsBulkActions ( ) ;
@@ -1399,19 +1440,22 @@ function updateJobsTable(jobs) {
13991440
14001441 return `
14011442 <tr>
1402- <td class="col-checkbox">
1443+ <td class="col-checkbox" style="padding: 4px 2px;" >
14031444 <input type="checkbox" ${ canStop ? '' : 'disabled' } ${ isSelected ? 'checked' : '' } onchange="toggleJobSelection('${ job . id } ', this.checked)">
14041445 </td>
1405- <td style="font-family: monospace; font-size: 12px;">
1406- ${ job . id }
1446+ <td style="font-family: monospace; font-size: 12px; padding: 4px 6px;">
1447+ <span onclick="copyToClipboard('${ job . id } '); return false;" style="cursor: pointer;" title="Click to copy">${ job . id } </span>
1448+ <a href="https://wandb.ai/metta-research/metta/runs/${ job . experiment_id } " target="_blank" class="wandb-link" onclick="event.stopPropagation();" title="Open in W&B" style="margin-left: 6px;">w&b</a>
14071449 <a href="https://skypilot-api.softmax-research.net/dashboard/jobs/${ job . id } " target="_blank" class="wandb-link" onclick="event.stopPropagation();" title="Open in SkyPilot Dashboard" style="margin-left: 6px;">sky</a>
14081450 <a href="https://app.datadoghq.com/logs?query=metta_run_id%3A%22${ job . experiment_id } %22" target="_blank" class="wandb-link" onclick="event.stopPropagation();" title="Open in Datadog" style="margin-left: 6px;">log</a>
14091451 </td>
1410- <td style="max-width: 300px; overflow: hidden; text-overflow: ellipsis;" title="${ job . experiment_id } ">${ job . experiment_id } </td>
1411- <td><span class="status-badge ${ job . status . toLowerCase ( ) } " title="${ job . status } ">${ abbreviateStatus ( job . status ) } </span></td>
1412- <td style="font-family: monospace; font-size: 11px;">${ resources } </td>
1413- <td style="font-size: 11px;">${ formatDuration ( job . started_at ) } </td>
1414- <td style="font-size: 11px;">${ job . started_at ? formatTime ( job . started_at ) : 'Not started' } </td>
1452+ <td style="max-width: 300px; overflow: hidden; text-overflow: ellipsis; padding: 4px 6px;">
1453+ <span onclick="copyToClipboard('${ job . experiment_id . replace ( / ' / g, "\\'" ) } '); return false;" style="cursor: pointer;" title="Click to copy">${ job . experiment_id } </span>
1454+ </td>
1455+ <td style="padding: 4px 6px;"><span class="status-badge ${ job . status . toLowerCase ( ) } " title="${ job . status } ">${ abbreviateStatus ( job . status ) } </span></td>
1456+ <td style="font-family: monospace; font-size: 11px; padding: 4px 6px;">${ resources } </td>
1457+ <td style="font-size: 11px; padding: 4px 6px;">${ formatDuration ( job . started_at ) } </td>
1458+ <td style="font-size: 11px; padding: 4px 6px;">${ job . started_at ? formatTime ( job . started_at ) : 'Not started' } </td>
14151459 </tr>
14161460 ` ;
14171461 } ) . join ( '' ) ;
@@ -1541,7 +1585,6 @@ async function duplicateExperiment(experimentId) {
15411585 name : newId ,
15421586 base_command : exp . base_command ,
15431587 tool_path : exp . tool_path ,
1544- run_name : exp . run_name ,
15451588 git_branch : exp . git_branch ,
15461589 nodes : exp . nodes ,
15471590 gpus : exp . gpus ,
@@ -1830,13 +1873,3 @@ async function saveExperimentOrder(order) {
18301873 console . error ( 'Error saving experiment order:' , error ) ;
18311874 }
18321875}
1833-
1834- // Copy to clipboard function
1835- function copyToClipboard ( text , message ) {
1836- navigator . clipboard . writeText ( text ) . then ( ( ) => {
1837- showNotification ( message || 'Copied to clipboard!' , 'success' ) ;
1838- } ) . catch ( err => {
1839- console . error ( 'Failed to copy:' , err ) ;
1840- showNotification ( 'Failed to copy to clipboard' , 'error' ) ;
1841- } ) ;
1842- }
0 commit comments