@@ -121,6 +121,7 @@ class Dashboard {
121121 <div class="dashboard-summary-actions">
122122 <button class="dashboard-topbar-btn" id="dashboard-open-telemetry-details" title="View trends and histograms">📈 Details</button>
123123 <button class="dashboard-topbar-btn" id="dashboard-open-performance" title="Per-terminal resource usage">⚙ Perf</button>
124+ <button class="dashboard-topbar-btn" id="dashboard-open-polecats" title="Manage sessions (restart/kill/logs)">🐾 Polecats</button>
124125 <button class="dashboard-topbar-btn" id="dashboard-open-tests" title="Run tests across worktrees">🧪 Tests</button>
125126 <button class="dashboard-topbar-btn" id="dashboard-export-telemetry" title="Download telemetry CSV export">⬇ Export</button>
126127 <button class="dashboard-topbar-btn" id="dashboard-export-telemetry-json" title="Download telemetry JSON export">⬇ JSON</button>
@@ -205,6 +206,10 @@ class Dashboard {
205206 e . preventDefault ( ) ;
206207 this . showPerformanceOverlay ( ) ;
207208 } ) ;
209+ document . getElementById ( 'dashboard-open-polecats' ) ?. addEventListener ( 'click' , ( e ) => {
210+ e . preventDefault ( ) ;
211+ this . showPolecatOverlay ( ) . catch ( ( ) => { } ) ;
212+ } ) ;
208213 document . getElementById ( 'dashboard-open-tests' ) ?. addEventListener ( 'click' , ( e ) => {
209214 e . preventDefault ( ) ;
210215 try {
@@ -822,6 +827,201 @@ class Dashboard {
822827 </tbody>
823828 </table>
824829 ` ;
830+ }
831+
832+ async showPolecatOverlay ( ) {
833+ const existing = document . getElementById ( 'dashboard-polecats-overlay' ) ;
834+ if ( existing ) {
835+ existing . classList . remove ( 'hidden' ) ;
836+ return ;
837+ }
838+
839+ const overlay = document . createElement ( 'div' ) ;
840+ overlay . id = 'dashboard-polecats-overlay' ;
841+ overlay . className = 'dashboard-telemetry-overlay' ;
842+ overlay . innerHTML = `
843+ <div class="dashboard-telemetry-panel" role="dialog" aria-label="Polecat management">
844+ <div class="dashboard-telemetry-header">
845+ <div class="dashboard-telemetry-title">Polecats — Sessions</div>
846+ <button class="dashboard-topbar-btn" id="dashboard-polecats-close" title="Close (Esc)">✕</button>
847+ </div>
848+ <div class="dashboard-telemetry-controls">
849+ <div class="dashboard-telemetry-actions">
850+ <button class="btn-secondary" type="button" id="dashboard-polecats-refresh">Refresh</button>
851+ </div>
852+ </div>
853+ <div id="dashboard-polecats-body" class="dashboard-telemetry-body">Loading…</div>
854+ </div>
855+ ` ;
856+
857+ document . body . appendChild ( overlay ) ;
858+
859+ const close = ( ) => this . hidePolecatOverlay ( ) ;
860+ overlay . addEventListener ( 'click' , ( e ) => {
861+ if ( e . target === overlay ) close ( ) ;
862+ } ) ;
863+ overlay . querySelector ( '#dashboard-polecats-close' ) ?. addEventListener ( 'click' , close ) ;
864+ overlay . querySelector ( '#dashboard-polecats-refresh' ) ?. addEventListener ( 'click' , ( ) => {
865+ this . loadPolecatDetails ( ) . catch ( ( ) => { } ) ;
866+ } ) ;
867+
868+ const onKey = ( e ) => {
869+ if ( e . key !== 'Escape' ) return ;
870+ const el = document . getElementById ( 'dashboard-polecats-overlay' ) ;
871+ if ( ! el || el . classList . contains ( 'hidden' ) ) return ;
872+ close ( ) ;
873+ } ;
874+ overlay . _escHandler = onKey ;
875+ document . addEventListener ( 'keydown' , onKey ) ;
876+
877+ await this . loadPolecatDetails ( ) ;
878+ }
879+
880+ hidePolecatOverlay ( ) {
881+ const overlay = document . getElementById ( 'dashboard-polecats-overlay' ) ;
882+ if ( ! overlay ) return ;
883+ overlay . classList . add ( 'hidden' ) ;
884+ const handler = overlay . _escHandler ;
885+ if ( handler ) {
886+ document . removeEventListener ( 'keydown' , handler ) ;
887+ overlay . _escHandler = null ;
888+ }
889+ overlay . remove ( ) ;
890+ }
891+
892+ async loadPolecatDetails ( ) {
893+ const bodyEl = document . getElementById ( 'dashboard-polecats-body' ) ;
894+ if ( ! bodyEl ) return ;
895+
896+ const escapeHtml = ( value ) => String ( value ?? '' )
897+ . replace ( / & / g, '&' )
898+ . replace ( / < / g, '<' )
899+ . replace ( / > / g, '>' ) ;
900+
901+ const sessions = Array . from ( this . orchestrator ?. sessions ?. entries ?. ( ) || [ ] ) ;
902+ sessions . sort ( ( a , b ) => String ( a [ 0 ] ) . localeCompare ( String ( b [ 0 ] ) ) ) ;
903+
904+ if ( ! sessions . length ) {
905+ bodyEl . textContent = 'No sessions.' ;
906+ return ;
907+ }
908+
909+ const state = {
910+ selected : sessions [ 0 ] ?. [ 0 ] || ''
911+ } ;
912+
913+ const render = async ( ) => {
914+ const selected = state . selected ;
915+ const selectedSession = this . orchestrator ?. sessions ?. get ?. ( selected ) || null ;
916+ const selectedTitle = selectedSession ? `${ selected } (${ selectedSession . type || '' } )` : selected ;
917+
918+ const rows = sessions . map ( ( [ id , s ] ) => {
919+ const status = escapeHtml ( s ?. status || 'idle' ) ;
920+ const branch = escapeHtml ( s ?. branch || '' ) ;
921+ const type = escapeHtml ( s ?. type || '' ) ;
922+ const worktreeId = escapeHtml ( s ?. worktreeId || '' ) ;
923+ const repo = escapeHtml ( s ?. repositoryName || '' ) ;
924+ const label = repo ? `${ repo } /${ worktreeId || '' } ` : ( worktreeId || '' ) ;
925+ const isSel = id === selected ;
926+ return `
927+ <tr data-polecat-session="${ escapeHtml ( id ) } " style="${ isSel ? 'background: rgba(255,255,255,0.04);' : '' } ">
928+ <td class="mono">${ escapeHtml ( id ) } </td>
929+ <td>${ escapeHtml ( label ) } </td>
930+ <td>${ type } </td>
931+ <td>${ status } </td>
932+ <td class="mono">${ branch } </td>
933+ <td style="white-space:nowrap;">
934+ <button class="btn-secondary" type="button" data-polecat-restart="${ escapeHtml ( id ) } " title="Restart session">↻</button>
935+ <button class="btn-secondary" type="button" data-polecat-kill="${ escapeHtml ( id ) } " title="Kill/close session">✕</button>
936+ </td>
937+ </tr>
938+ ` ;
939+ } ) . join ( '' ) ;
940+
941+ bodyEl . innerHTML = `
942+ <div style="display:flex; gap:12px; align-items:stretch; min-height: 50vh;">
943+ <div style="flex: 0 0 min(720px, 58vw); min-width: 320px; overflow:auto;">
944+ <table class="worktree-inspector-table">
945+ <thead>
946+ <tr>
947+ <th>Session</th>
948+ <th>Worktree</th>
949+ <th>Type</th>
950+ <th>Status</th>
951+ <th>Branch</th>
952+ <th>Actions</th>
953+ </tr>
954+ </thead>
955+ <tbody>
956+ ${ rows }
957+ </tbody>
958+ </table>
959+ </div>
960+ <div style="flex:1; min-width: 260px; display:flex; flex-direction:column;">
961+ <div style="display:flex; align-items:center; justify-content:space-between; gap:10px; margin-bottom:8px;">
962+ <div class="mono" style="min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">${ escapeHtml ( selectedTitle ) } </div>
963+ <button class="btn-secondary" type="button" id="dashboard-polecats-log-refresh" ${ selected ? '' : 'disabled' } >Refresh log</button>
964+ </div>
965+ <pre id="dashboard-polecats-log" style="flex:1; margin:0; padding:10px; border-radius:8px; border:1px solid var(--border-color); background: rgba(0,0,0,0.25); overflow:auto; white-space:pre-wrap; word-break:break-word;">Loading…</pre>
966+ </div>
967+ </div>
968+ ` ;
969+
970+ const loadLog = async ( ) => {
971+ const pre = bodyEl . querySelector ( '#dashboard-polecats-log' ) ;
972+ if ( ! pre ) return ;
973+ if ( ! selected ) {
974+ pre . textContent = 'No session selected.' ;
975+ return ;
976+ }
977+ pre . textContent = 'Loading…' ;
978+ try {
979+ const res = await fetch ( `/api/sessions/${ encodeURIComponent ( selected ) } /log?tailChars=20000` ) ;
980+ const data = await res . json ( ) . catch ( ( ) => ( { } ) ) ;
981+ if ( ! res . ok || ! data ?. ok ) throw new Error ( data ?. error || 'Failed to load log' ) ;
982+ pre . textContent = String ( data . log || '' ) ;
983+ } catch ( err ) {
984+ pre . textContent = `Failed to load: ${ String ( err ?. message || err ) } ` ;
985+ }
986+ } ;
987+
988+ await loadLog ( ) ;
989+
990+ bodyEl . querySelector ( '#dashboard-polecats-log-refresh' ) ?. addEventListener ( 'click' , ( e ) => {
991+ e . preventDefault ( ) ;
992+ loadLog ( ) . catch ( ( ) => { } ) ;
993+ } ) ;
994+
995+ bodyEl . querySelectorAll ( '[data-polecat-session]' ) . forEach ( ( row ) => {
996+ row . addEventListener ( 'click' , ( ) => {
997+ const id = row . getAttribute ( 'data-polecat-session' ) ;
998+ if ( ! id ) return ;
999+ state . selected = id ;
1000+ render ( ) . catch ( ( ) => { } ) ;
1001+ } ) ;
1002+ } ) ;
1003+
1004+ bodyEl . querySelectorAll ( 'button[data-polecat-restart]' ) . forEach ( ( btn ) => {
1005+ btn . addEventListener ( 'click' , ( e ) => {
1006+ e . preventDefault ( ) ;
1007+ e . stopPropagation ( ) ;
1008+ const id = btn . getAttribute ( 'data-polecat-restart' ) ;
1009+ if ( ! id ) return ;
1010+ this . orchestrator ?. socket ?. emit ?. ( 'restart-session' , { sessionId : id } ) ;
1011+ } ) ;
1012+ } ) ;
1013+ bodyEl . querySelectorAll ( 'button[data-polecat-kill]' ) . forEach ( ( btn ) => {
1014+ btn . addEventListener ( 'click' , ( e ) => {
1015+ e . preventDefault ( ) ;
1016+ e . stopPropagation ( ) ;
1017+ const id = btn . getAttribute ( 'data-polecat-kill' ) ;
1018+ if ( ! id ) return ;
1019+ this . orchestrator ?. socket ?. emit ?. ( 'destroy-session' , { sessionId : id } ) ;
1020+ } ) ;
1021+ } ) ;
1022+ } ;
1023+
1024+ await render ( ) ;
8251025 }
8261026
8271027 hidePerformanceOverlay ( ) {
0 commit comments