diff --git a/plugin/server/api.go b/plugin/server/api.go index 25b759f..1bb97d8 100644 --- a/plugin/server/api.go +++ b/plugin/server/api.go @@ -6,6 +6,7 @@ import ( "io" "net/http" "strings" + "time" "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/plugin" @@ -13,19 +14,24 @@ import ( // ServeHTTP handles HTTP requests to the plugin. func (p *Plugin) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Request) { - // Auth middleware: validate shared secret - config := p.getConfiguration() - if config.SharedSecret != "" { - auth := r.Header.Get("Authorization") - expected := "Bearer " + config.SharedSecret - if auth != expected { - http.Error(w, `{"error": "unauthorized"}`, http.StatusUnauthorized) - return + path := r.URL.Path + + // Auth middleware: validate shared secret for write operations. + // Read-only endpoints (GET /sessions, GET /health) are accessible to any + // authenticated Mattermost user — no shared secret required. + isReadOnly := r.Method == http.MethodGet && (path == "/api/v1/sessions" || path == "/api/v1/health") + if !isReadOnly { + config := p.getConfiguration() + if config.SharedSecret != "" { + auth := r.Header.Get("Authorization") + expected := "Bearer " + config.SharedSecret + if auth != expected { + http.Error(w, `{"error": "unauthorized"}`, http.StatusUnauthorized) + return + } } } - path := r.URL.Path - switch { case path == "/api/v1/health" && r.Method == http.MethodGet: p.handleHealth(w, r) @@ -46,10 +52,14 @@ func (p *Plugin) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Req // handleHealth returns plugin health status. func (p *Plugin) handleHealth(w http.ResponseWriter, r *http.Request) { - sessions, err := p.store.ListActiveSessions() + sessions, err := p.store.ListAllSessions() count := 0 if err == nil { - count = len(sessions) + for _, s := range sessions { + if s.Status == "active" { + count++ + } + } } writeJSON(w, http.StatusOK, map[string]interface{}{ @@ -59,9 +69,9 @@ func (p *Plugin) handleHealth(w http.ResponseWriter, r *http.Request) { }) } -// handleListSessions returns all active sessions. +// handleListSessions returns all sessions (active and non-active). func (p *Plugin) handleListSessions(w http.ResponseWriter, r *http.Request) { - sessions, err := p.store.ListActiveSessions() + sessions, err := p.store.ListAllSessions() if err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return @@ -93,8 +103,14 @@ func (p *Plugin) handleCreateSession(w http.ResponseWriter, r *http.Request) { // Check max active sessions config := p.getConfiguration() - sessions, _ := p.store.ListActiveSessions() - if len(sessions) >= config.MaxActiveSessions { + allSessions, _ := p.store.ListAllSessions() + activeCount := 0 + for _, s := range allSessions { + if s.Status == "active" { + activeCount++ + } + } + if activeCount >= config.MaxActiveSessions { writeJSON(w, http.StatusTooManyRequests, map[string]string{"error": "max active sessions reached"}) return } @@ -133,14 +149,17 @@ func (p *Plugin) handleCreateSession(w http.ResponseWriter, r *http.Request) { } // Store session data + now := time.Now().UnixMilli() sessionData := SessionData{ - SessionKey: req.SessionKey, - PostID: createdPost.Id, - ChannelID: req.ChannelID, - RootID: req.RootID, - AgentID: req.AgentID, - Status: "active", - Lines: []string{}, + SessionKey: req.SessionKey, + PostID: createdPost.Id, + ChannelID: req.ChannelID, + RootID: req.RootID, + AgentID: req.AgentID, + Status: "active", + Lines: []string{}, + StartTimeMs: now, + LastUpdateMs: now, } if err := p.store.SaveSession(req.SessionKey, sessionData); err != nil { p.API.LogWarn("Failed to save session", "error", err.Error()) @@ -187,6 +206,7 @@ func (p *Plugin) handleUpdateSession(w http.ResponseWriter, r *http.Request, ses existing.ElapsedMs = req.ElapsedMs existing.TokenCount = req.TokenCount existing.Children = req.Children + existing.LastUpdateMs = time.Now().UnixMilli() if req.StartTimeMs > 0 { existing.StartTimeMs = req.StartTimeMs } diff --git a/plugin/server/plugin.go b/plugin/server/plugin.go index 8b3d35f..3350a2d 100644 --- a/plugin/server/plugin.go +++ b/plugin/server/plugin.go @@ -2,6 +2,7 @@ package main import ( "sync" + "time" "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/plugin" @@ -22,6 +23,9 @@ type Plugin struct { // botUserID is the plugin's bot user ID (created on activation). botUserID string + + // stopCleanup signals the cleanup goroutine to stop. + stopCleanup chan struct{} } // OnActivate is called when the plugin is activated. @@ -41,10 +45,36 @@ func (p *Plugin) OnActivate() error { p.API.LogInfo("Plugin bot user ensured", "botUserID", botID) } + // Start session cleanup goroutine + p.stopCleanup = make(chan struct{}) + go p.sessionCleanupLoop() + p.API.LogInfo("OpenClaw Live Status plugin activated") return nil } +// sessionCleanupLoop runs periodically to clean up stale and expired sessions. +func (p *Plugin) sessionCleanupLoop() { + ticker := time.NewTicker(5 * time.Minute) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + staleThreshold := int64(30 * 60 * 1000) // 30 minutes — active sessions with no update + expireThreshold := int64(60 * 60 * 1000) // 1 hour — completed/interrupted sessions + cleaned, expired, err := p.store.CleanStaleSessions(staleThreshold, expireThreshold) + if err != nil { + p.API.LogWarn("Session cleanup error", "error", err.Error()) + } else if cleaned > 0 || expired > 0 { + p.API.LogInfo("Session cleanup completed", "stale_marked", cleaned, "expired_deleted", expired) + } + case <-p.stopCleanup: + return + } + } +} + // getBotUserID returns the plugin's bot user ID. func (p *Plugin) getBotUserID() string { return p.botUserID @@ -52,8 +82,13 @@ func (p *Plugin) getBotUserID() string { // OnDeactivate is called when the plugin is deactivated. func (p *Plugin) OnDeactivate() error { + // Stop cleanup goroutine + if p.stopCleanup != nil { + close(p.stopCleanup) + } + // Mark all active sessions as interrupted - sessions, err := p.store.ListActiveSessions() + sessions, err := p.store.ListAllSessions() if err != nil { p.API.LogWarn("Failed to list sessions on deactivate", "error", err.Error()) } else { diff --git a/plugin/server/store.go b/plugin/server/store.go index d5384f3..0c04f32 100644 --- a/plugin/server/store.go +++ b/plugin/server/store.go @@ -5,6 +5,7 @@ import ( "fmt" "net/url" "strings" + "time" "github.com/mattermost/mattermost/server/public/plugin" ) @@ -22,8 +23,9 @@ type SessionData struct { Lines []string `json:"lines"` ElapsedMs int64 `json:"elapsed_ms"` TokenCount int `json:"token_count"` - Children []SessionData `json:"children,omitempty"` - StartTimeMs int64 `json:"start_time_ms"` + Children []SessionData `json:"children,omitempty"` + StartTimeMs int64 `json:"start_time_ms"` + LastUpdateMs int64 `json:"last_update_ms"` } // Store wraps Mattermost KV store operations for session persistence. @@ -79,8 +81,8 @@ func (s *Store) DeleteSession(sessionKey string) error { return nil } -// ListActiveSessions returns all active sessions from the KV store. -func (s *Store) ListActiveSessions() ([]SessionData, error) { +// ListAllSessions returns all sessions from the KV store (active and non-active). +func (s *Store) ListAllSessions() ([]SessionData, error) { var sessions []SessionData page := 0 perPage := 100 @@ -106,12 +108,59 @@ func (s *Store) ListActiveSessions() ([]SessionData, error) { if err := json.Unmarshal(b, &data); err != nil { continue } - if data.Status == "active" { - sessions = append(sessions, data) - } + sessions = append(sessions, data) } page++ } return sessions, nil } + +// ListActiveSessions returns only active sessions from the KV store. +func (s *Store) ListActiveSessions() ([]SessionData, error) { + all, err := s.ListAllSessions() + if err != nil { + return nil, err + } + var active []SessionData + for _, sess := range all { + if sess.Status == "active" { + active = append(active, sess) + } + } + return active, nil +} + +// CleanStaleSessions marks stale active sessions as interrupted and deletes expired completed sessions. +// staleThresholdMs: active sessions with no update for this long are marked interrupted. +// expireThresholdMs: non-active sessions older than this are deleted from KV. +func (s *Store) CleanStaleSessions(staleThresholdMs, expireThresholdMs int64) (cleaned int, expired int, err error) { + now := time.Now().UnixMilli() + all, err := s.ListAllSessions() + if err != nil { + return 0, 0, err + } + for _, session := range all { + lastUpdate := session.LastUpdateMs + if lastUpdate == 0 { + lastUpdate = session.StartTimeMs + } + if lastUpdate == 0 { + continue + } + age := now - lastUpdate + + if session.Status == "active" && age > staleThresholdMs { + // Mark stale sessions as interrupted + session.Status = "interrupted" + session.LastUpdateMs = now + _ = s.SaveSession(session.SessionKey, session) + cleaned++ + } else if session.Status != "active" && age > expireThresholdMs { + // Delete expired completed/interrupted sessions + _ = s.DeleteSession(session.SessionKey) + expired++ + } + } + return cleaned, expired, nil +} diff --git a/plugin/webapp/dist/main.js b/plugin/webapp/dist/main.js index 4eb5962..35fa813 100644 --- a/plugin/webapp/dist/main.js +++ b/plugin/webapp/dist/main.js @@ -1 +1 @@ -!function(e,n){if("object"==typeof exports&&"object"==typeof module)module.exports=n(require("React"));else if("function"==typeof define&&define.amd)define(["React"],n);else{var t="object"==typeof exports?n(require("React")):n(e.React);for(var s in t)("object"==typeof exports?exports:e)[s]=t[s]}}(self,e=>(()=>{"use strict";var n={999(e,n,t){t.d(n,{A:()=>l});var s=t(601),a=t.n(s),r=t(314),o=t.n(r)()(a());o.push([e.id,"/* OpenClaw Live Status — Post Type Styles */\n\n.ls-post {\n border-radius: 4px;\n overflow: hidden;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n margin: 4px 0;\n}\n\n/* Header */\n.ls-header {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 8px 12px;\n background: var(--center-channel-bg, #fff);\n border-bottom: 1px solid var(--center-channel-color-08, rgba(0, 0, 0, 0.08));\n}\n\n.ls-agent-badge {\n font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;\n font-size: 12px;\n font-weight: 600;\n padding: 2px 8px;\n border-radius: 10px;\n background: var(--button-bg, #166de0);\n color: var(--button-color, #fff);\n}\n\n.ls-child-badge {\n font-size: 11px;\n background: var(--center-channel-color-24, rgba(0, 0, 0, 0.24));\n}\n\n.ls-status-badge {\n font-size: 11px;\n font-weight: 600;\n padding: 2px 6px;\n border-radius: 3px;\n text-transform: uppercase;\n}\n\n.ls-status-active .ls-status-badge {\n background: #e8f5e9;\n color: #2e7d32;\n}\n\n.ls-status-done .ls-status-badge {\n background: #e3f2fd;\n color: #1565c0;\n}\n\n.ls-status-error .ls-status-badge {\n background: #fbe9e7;\n color: #c62828;\n}\n\n.ls-status-interrupted .ls-status-badge {\n background: #fff3e0;\n color: #e65100;\n}\n\n.ls-elapsed {\n font-size: 12px;\n color: var(--center-channel-color-56, rgba(0, 0, 0, 0.56));\n margin-left: auto;\n font-family: 'SFMono-Regular', Consolas, monospace;\n}\n\n/* Live dot — pulsing green indicator */\n.ls-live-dot {\n width: 8px;\n height: 8px;\n border-radius: 50%;\n background: #4caf50;\n animation: ls-pulse 1.5s ease-in-out infinite;\n}\n\n@keyframes ls-pulse {\n 0%,\n 100% {\n opacity: 1;\n transform: scale(1);\n }\n 50% {\n opacity: 0.5;\n transform: scale(0.8);\n }\n}\n\n/* Terminal view */\n.ls-terminal {\n max-height: 400px;\n overflow-y: auto;\n padding: 8px 12px;\n background: var(--center-channel-color-04, rgba(0, 0, 0, 0.04));\n font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;\n font-size: 12px;\n line-height: 1.6;\n}\n\n/* Status lines */\n.ls-status-line {\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.ls-tool-call {\n color: var(--center-channel-color, #3d3c40);\n}\n\n.ls-tool-name {\n color: var(--link-color, #2389d7);\n font-weight: 600;\n}\n\n.ls-tool-args {\n color: var(--center-channel-color-72, rgba(0, 0, 0, 0.72));\n}\n\n.ls-marker-ok {\n color: #4caf50;\n font-weight: 600;\n}\n\n.ls-marker-err {\n color: #f44336;\n font-weight: 600;\n}\n\n.ls-thinking {\n color: var(--center-channel-color-48, rgba(0, 0, 0, 0.48));\n}\n\n.ls-thinking-prefix {\n color: var(--center-channel-color-24, rgba(0, 0, 0, 0.24));\n}\n\n/* Children (sub-agents) */\n.ls-children {\n border-top: 1px solid var(--center-channel-color-08, rgba(0, 0, 0, 0.08));\n padding: 4px 0;\n}\n\n.ls-child {\n margin: 0 12px;\n border-left: 2px solid var(--center-channel-color-16, rgba(0, 0, 0, 0.16));\n padding-left: 8px;\n}\n\n.ls-child-header {\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 4px 0;\n font-size: 12px;\n}\n\n.ls-expand-icon {\n font-size: 10px;\n color: var(--center-channel-color-48, rgba(0, 0, 0, 0.48));\n width: 12px;\n}\n\n/* Footer */\n.ls-footer {\n padding: 4px 12px 8px;\n font-size: 11px;\n color: var(--center-channel-color-48, rgba(0, 0, 0, 0.48));\n font-family: 'SFMono-Regular', Consolas, monospace;\n}\n\n/* Loading state */\n.ls-loading {\n padding: 12px;\n color: var(--center-channel-color-48, rgba(0, 0, 0, 0.48));\n font-style: italic;\n}\n\n/* Scrollbar styling */\n.ls-terminal::-webkit-scrollbar {\n width: 6px;\n}\n\n.ls-terminal::-webkit-scrollbar-track {\n background: transparent;\n}\n\n.ls-terminal::-webkit-scrollbar-thumb {\n background: var(--center-channel-color-16, rgba(0, 0, 0, 0.16));\n border-radius: 3px;\n}\n\n.ls-terminal::-webkit-scrollbar-thumb:hover {\n background: var(--center-channel-color-32, rgba(0, 0, 0, 0.32));\n}\n\n/* ========= RHS Panel Styles ========= */\n\n.ls-rhs-panel {\n display: flex;\n flex-direction: column;\n height: 100%;\n overflow-y: auto;\n background: var(--center-channel-bg, #fff);\n}\n\n.ls-rhs-summary {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 12px 16px;\n border-bottom: 1px solid var(--center-channel-color-08, rgba(0, 0, 0, 0.08));\n font-size: 13px;\n font-weight: 600;\n color: var(--center-channel-color, #3d3c40);\n}\n\n.ls-rhs-count {\n display: flex;\n align-items: center;\n gap: 6px;\n}\n\n.ls-rhs-empty {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n padding: 48px 24px;\n text-align: center;\n}\n\n.ls-rhs-empty-icon {\n font-size: 32px;\n margin-bottom: 12px;\n}\n\n.ls-rhs-empty-text {\n color: var(--center-channel-color-48, rgba(0, 0, 0, 0.48));\n font-size: 13px;\n line-height: 1.5;\n}\n\n.ls-rhs-card {\n margin: 8px 12px;\n border: 1px solid var(--center-channel-color-08, rgba(0, 0, 0, 0.08));\n border-radius: 6px;\n overflow: hidden;\n}\n\n.ls-rhs-card.ls-status-active {\n border-color: rgba(76, 175, 80, 0.3);\n}\n\n.ls-rhs-card-header {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 8px 10px;\n background: var(--center-channel-color-04, rgba(0, 0, 0, 0.04));\n border-bottom: 1px solid var(--center-channel-color-08, rgba(0, 0, 0, 0.08));\n}\n\n.ls-rhs-card .ls-terminal {\n max-height: 250px;\n font-size: 11px;\n padding: 6px 10px;\n}\n\n.ls-rhs-card-footer {\n padding: 4px 10px 6px;\n font-size: 11px;\n color: var(--center-channel-color-48, rgba(0, 0, 0, 0.48));\n font-family: 'SFMono-Regular', Consolas, monospace;\n border-top: 1px solid var(--center-channel-color-08, rgba(0, 0, 0, 0.08));\n}\n\n.ls-rhs-children {\n padding: 4px 10px;\n border-top: 1px solid var(--center-channel-color-08, rgba(0, 0, 0, 0.08));\n}\n\n.ls-rhs-child {\n padding: 4px 0;\n}\n\n.ls-rhs-child-header {\n display: flex;\n align-items: center;\n gap: 6px;\n font-size: 11px;\n}\n\n.ls-rhs-section {\n margin-top: 4px;\n}\n\n.ls-rhs-section-title {\n padding: 8px 16px 4px;\n font-size: 11px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n color: var(--center-channel-color-48, rgba(0, 0, 0, 0.48));\n}\n",""]);const l=o},314(e){e.exports=function(e){var n=[];return n.toString=function(){return this.map(function(n){var t="",s=void 0!==n[5];return n[4]&&(t+="@supports (".concat(n[4],") {")),n[2]&&(t+="@media ".concat(n[2]," {")),s&&(t+="@layer".concat(n[5].length>0?" ".concat(n[5]):""," {")),t+=e(n),s&&(t+="}"),n[2]&&(t+="}"),n[4]&&(t+="}"),t}).join("")},n.i=function(e,t,s,a,r){"string"==typeof e&&(e=[[null,e,void 0]]);var o={};if(s)for(var l=0;l0?" ".concat(d[5]):""," {").concat(d[1],"}")),d[5]=r),t&&(d[2]?(d[1]="@media ".concat(d[2]," {").concat(d[1],"}"),d[2]=t):d[2]=t),a&&(d[4]?(d[1]="@supports (".concat(d[4],") {").concat(d[1],"}"),d[4]=a):d[4]="".concat(a)),n.push(d))}},n}},601(e){e.exports=function(e){return e[1]}},72(e){var n=[];function t(e){for(var t=-1,s=0;s0?" ".concat(t.layer):""," {")),s+=t.css,a&&(s+="}"),t.media&&(s+="}"),t.supports&&(s+="}");var r=t.sourceMap;r&&"undefined"!=typeof btoa&&(s+="\n/*# sourceMappingURL=data:application/json;base64,".concat(btoa(unescape(encodeURIComponent(JSON.stringify(r))))," */")),n.styleTagTransform(s,e,n.options)}(n,e,t)},remove:function(){!function(e){if(null===e.parentNode)return!1;e.parentNode.removeChild(e)}(n)}}}},113(e){e.exports=function(e,n){if(n.styleSheet)n.styleSheet.cssText=e;else{for(;n.firstChild;)n.removeChild(n.firstChild);n.appendChild(document.createTextNode(e))}}},883(n){n.exports=e}},t={};function s(e){var a=t[e];if(void 0!==a)return a.exports;var r=t[e]={id:e,exports:{}};return n[e](r,r.exports,s),r.exports}s.n=e=>{var n=e&&e.__esModule?()=>e.default:()=>e;return s.d(n,{a:n}),n},s.d=(e,n)=>{for(var t in n)s.o(n,t)&&!s.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:n[t]})},s.o=(e,n)=>Object.prototype.hasOwnProperty.call(e,n),s.nc=void 0;var a=s(883),r=s.n(a);const o=({line:e})=>{const n=e.match(/^(\S+?): (.+?)( \[OK\]| \[ERR\])?$/);if(n){const e=n[1],t=n[2],s=(n[3]||"").trim();return r().createElement("div",{className:"ls-status-line ls-tool-call"},r().createElement("span",{className:"ls-tool-name"},e,":"),r().createElement("span",{className:"ls-tool-args"}," ",t),s&&r().createElement("span",{className:"[OK]"===s?"ls-marker-ok":"ls-marker-err"}," ",s))}return r().createElement("div",{className:"ls-status-line ls-thinking"},r().createElement("span",{className:"ls-thinking-prefix"},"│"," "),r().createElement("span",{className:"ls-thinking-text"},e))},l=({lines:e,maxLines:n=30})=>{const t=(0,a.useRef)(null),[s,l]=(0,a.useState)(!1),i=(0,a.useRef)(null);(0,a.useEffect)(()=>{!s&&i.current&&i.current.scrollIntoView({behavior:"smooth"})},[e.length,s]);const c=e.slice(-n);return r().createElement("div",{className:"ls-terminal",ref:t,onScroll:()=>{if(!t.current)return;const{scrollTop:e,scrollHeight:n,clientHeight:s}=t.current;l(!(n-e-s<20))}},c.map((e,n)=>r().createElement(o,{key:n,line:e})),r().createElement("div",{ref:i}))};function i(e){e<0&&(e=0);const n=Math.floor(e/1e3),t=Math.floor(n/60),s=Math.floor(t/60);return s>0?`${s}h${t%60}m`:t>0?`${t}m${n%60}s`:`${n}s`}"undefined"!=typeof window&&(window.__livestatus_updates=window.__livestatus_updates||{},window.__livestatus_listeners=window.__livestatus_listeners||{});const c=({child:e})=>{const[n,t]=(0,a.useState)(!1);return r().createElement("div",{className:"ls-child"},r().createElement("div",{className:"ls-child-header",onClick:()=>t(!n),style:{cursor:"pointer"}},r().createElement("span",{className:"ls-expand-icon"},n?"▼":"▶"),r().createElement("span",{className:"ls-agent-badge ls-child-badge"},e.agent_id),r().createElement("span",{className:`ls-status-badge ls-status-${e.status}`},e.status.toUpperCase()),r().createElement("span",{className:"ls-elapsed"},i(e.elapsed_ms))),n&&r().createElement(l,{lines:e.lines,maxLines:15}))},d=({post:e,theme:n})=>{const[t,s]=(0,a.useState)(0),o={session_key:e.props.session_key||"",post_id:e.id,agent_id:e.props.agent_id||"unknown",status:e.props.status||"active",lines:e.props.final_lines||[],elapsed_ms:e.props.elapsed_ms||0,token_count:e.props.token_count||0,children:[],start_time_ms:e.props.start_time_ms||0},d=function(e,n){const[t,s]=(0,a.useState)(window.__livestatus_updates[e]||n);return(0,a.useEffect)(()=>{window.__livestatus_listeners[e]||(window.__livestatus_listeners[e]=[]);const n=e=>s(e);return window.__livestatus_listeners[e].push(n),window.__livestatus_updates[e]&&s(window.__livestatus_updates[e]),()=>{const t=window.__livestatus_listeners[e];if(t){const e=t.indexOf(n);e>=0&&t.splice(e,1)}}},[e]),t}(e.id,o),p="active"===(null==d?void 0:d.status);if((0,a.useEffect)(()=>{if(!p||!(null==d?void 0:d.start_time_ms))return;const e=setInterval(()=>{s(Date.now()-d.start_time_ms)},1e3);return()=>clearInterval(e)},[p,null==d?void 0:d.start_time_ms]),!d)return r().createElement("div",{className:"ls-post ls-loading"},"Loading status...");const u=p&&d.start_time_ms?i(t||Date.now()-d.start_time_ms):i(d.elapsed_ms),m=`ls-status-${d.status}`;return r().createElement("div",{className:`ls-post ${m}`},r().createElement("div",{className:"ls-header"},r().createElement("span",{className:"ls-agent-badge"},d.agent_id),p&&r().createElement("span",{className:"ls-live-dot"}),r().createElement("span",{className:`ls-status-badge ${m}`},d.status.toUpperCase()),r().createElement("span",{className:"ls-elapsed"},u)),r().createElement(l,{lines:d.lines,maxLines:30}),d.children&&d.children.length>0&&r().createElement("div",{className:"ls-children"},d.children.map((e,n)=>r().createElement(c,{key:e.session_key||n,child:e}))),!p&&d.token_count>0&&r().createElement("div",{className:"ls-footer"},(f=d.token_count)>=1e6?`${(f/1e6).toFixed(1)}M`:f>=1e3?`${(f/1e3).toFixed(1)}k`:String(f)," tokens"));var f};function p(e){e<0&&(e=0);const n=Math.floor(e/1e3),t=Math.floor(n/60),s=Math.floor(t/60);return s>0?`${s}h${t%60}m`:t>0?`${t}m${n%60}s`:`${n}s`}const u=({data:e})=>{const[n,t]=(0,a.useState)(0),s="active"===e.status;(0,a.useEffect)(()=>{if(!s||!e.start_time_ms)return;const n=setInterval(()=>{t(Date.now()-e.start_time_ms)},1e3);return()=>clearInterval(n)},[s,e.start_time_ms]);const o=s&&e.start_time_ms?p(n||Date.now()-e.start_time_ms):p(e.elapsed_ms),i=`ls-status-${e.status}`;return r().createElement("div",{className:`ls-rhs-card ${i}`},r().createElement("div",{className:"ls-rhs-card-header"},r().createElement("span",{className:"ls-agent-badge"},e.agent_id),s&&r().createElement("span",{className:"ls-live-dot"}),r().createElement("span",{className:`ls-status-badge ${i}`},e.status.toUpperCase()),r().createElement("span",{className:"ls-elapsed"},o)),r().createElement(l,{lines:e.lines,maxLines:20}),e.children&&e.children.length>0&&r().createElement("div",{className:"ls-rhs-children"},e.children.map((e,n)=>r().createElement("div",{key:e.session_key||n,className:"ls-rhs-child"},r().createElement("div",{className:"ls-rhs-child-header"},r().createElement("span",{className:"ls-agent-badge ls-child-badge"},e.agent_id),r().createElement("span",{className:`ls-status-badge ls-status-${e.status}`},e.status.toUpperCase()),r().createElement("span",{className:"ls-elapsed"},p(e.elapsed_ms)))))),e.token_count>0&&r().createElement("div",{className:"ls-rhs-card-footer"},(c=e.token_count)>=1e6?`${(c/1e6).toFixed(1)}M`:c>=1e3?`${(c/1e3).toFixed(1)}k`:String(c)," tokens"));var c},m=()=>{const e=function(){const[e,n]=(0,a.useState)(()=>Object.assign({},window.__livestatus_updates||{}));return(0,a.useEffect)(()=>{const e="__rhs_panel__";window.__livestatus_listeners[e]||(window.__livestatus_listeners[e]=[]);const t=()=>{n(Object.assign({},window.__livestatus_updates||{}))};window.__livestatus_listeners[e].push(t);const s=setInterval(()=>{const e=window.__livestatus_updates||{};n(n=>{if(Object.keys(n).sort().join(",")!==Object.keys(e).sort().join(","))return Object.assign({},e);for(const t of Object.keys(e))if(e[t]!==n[t])return Object.assign({},e);return n})},2e3);return()=>{clearInterval(s);const n=window.__livestatus_listeners[e];if(n){const e=n.indexOf(t);e>=0&&n.splice(e,1)}}},[]),e}(),n=Object.values(e).sort((e,n)=>"active"===e.status&&"active"!==n.status?-1:"active"===n.status&&"active"!==e.status?1:(n.start_time_ms||0)-(e.start_time_ms||0)),t=n.filter(e=>"active"===e.status),s=n.filter(e=>"active"!==e.status);return r().createElement("div",{className:"ls-rhs-panel"},r().createElement("div",{className:"ls-rhs-summary"},r().createElement("span",{className:"ls-rhs-count"},t.length>0?r().createElement(r().Fragment,null,r().createElement("span",{className:"ls-live-dot"}),t.length," active"):"No active sessions")),0===t.length&&0===s.length&&r().createElement("div",{className:"ls-rhs-empty"},r().createElement("div",{className:"ls-rhs-empty-icon"},"⚙️"),r().createElement("div",{className:"ls-rhs-empty-text"},"No agent activity yet.",r().createElement("br",null),"Status will appear here when an agent starts working.")),t.map(e=>r().createElement(u,{key:e.post_id||e.session_key,data:e})),s.length>0&&r().createElement("div",{className:"ls-rhs-section"},r().createElement("div",{className:"ls-rhs-section-title"},"Recent"),s.slice(0,5).map(e=>r().createElement(u,{key:e.post_id||e.session_key,data:e}))))};var f=s(72),g=s.n(f),h=s(825),v=s.n(h),_=s(659),x=s.n(_),b=s(56),w=s.n(b),y=s(540),E=s.n(y),k=s(113),N=s.n(k),S=s(999),M={};M.styleTagTransform=N(),M.setAttributes=w(),M.insert=x().bind(null,"head"),M.domAPI=v(),M.insertStyleElement=E(),g()(S.A,M),S.A&&S.A.locals&&S.A.locals;const C="com.openclaw.livestatus",$=`custom_${C}_update`;return"undefined"!=typeof window&&(window.__livestatus_updates=window.__livestatus_updates||{},window.__livestatus_listeners=window.__livestatus_listeners||{}),window.registerPlugin(C,new class{constructor(){this.postTypeComponentId=null}initialize(e,n){this.postTypeComponentId=e.registerPostTypeComponent("custom_livestatus",d);const t=e.registerRightHandSidebarComponent({component:m,title:"Agent Status"}).toggleRHSPlugin;e.registerChannelHeaderButtonAction({icon:r().createElement("i",{className:"icon icon-cog-outline",style:{fontSize:"18px"}}),action:()=>{n.dispatch(t)},dropdownText:"Agent Status",tooltipText:"Toggle Agent Status panel"}),e.registerWebSocketEventHandler($,e=>{const n=e.data;if(!n||!n.post_id)return;const t={session_key:n.session_key,post_id:n.post_id,agent_id:n.agent_id,status:n.status,lines:n.lines||[],elapsed_ms:n.elapsed_ms||0,token_count:n.token_count||0,children:n.children||[],start_time_ms:n.start_time_ms||0};window.__livestatus_updates[n.post_id]=t;const s=window.__livestatus_listeners[n.post_id];s&&s.forEach(e=>e(t));const a=window.__livestatus_listeners.__rhs_panel__;a&&a.forEach(e=>e(t))})}uninitialize(){}}),{}})()); \ No newline at end of file +!function(e,n){if("object"==typeof exports&&"object"==typeof module)module.exports=n(require("React"));else if("function"==typeof define&&define.amd)define(["React"],n);else{var t="object"==typeof exports?n(require("React")):n(e.React);for(var s in t)("object"==typeof exports?exports:e)[s]=t[s]}}(self,e=>(()=>{"use strict";var n={999(e,n,t){t.d(n,{A:()=>l});var s=t(601),a=t.n(s),r=t(314),o=t.n(r)()(a());o.push([e.id,"/* OpenClaw Live Status — Post Type Styles */\n\n.ls-post {\n border-radius: 4px;\n overflow: hidden;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n margin: 4px 0;\n}\n\n/* Header */\n.ls-header {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 8px 12px;\n background: var(--center-channel-bg, #fff);\n border-bottom: 1px solid var(--center-channel-color-08, rgba(0, 0, 0, 0.08));\n}\n\n.ls-agent-badge {\n font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;\n font-size: 12px;\n font-weight: 600;\n padding: 2px 8px;\n border-radius: 10px;\n background: var(--button-bg, #166de0);\n color: var(--button-color, #fff);\n}\n\n.ls-child-badge {\n font-size: 11px;\n background: var(--center-channel-color-24, rgba(0, 0, 0, 0.24));\n}\n\n.ls-status-badge {\n font-size: 11px;\n font-weight: 600;\n padding: 2px 6px;\n border-radius: 3px;\n text-transform: uppercase;\n}\n\n.ls-status-active .ls-status-badge {\n background: #e8f5e9;\n color: #2e7d32;\n}\n\n.ls-status-done .ls-status-badge {\n background: #e3f2fd;\n color: #1565c0;\n}\n\n.ls-status-error .ls-status-badge {\n background: #fbe9e7;\n color: #c62828;\n}\n\n.ls-status-interrupted .ls-status-badge {\n background: #fff3e0;\n color: #e65100;\n}\n\n.ls-elapsed {\n font-size: 12px;\n color: var(--center-channel-color-56, rgba(0, 0, 0, 0.56));\n margin-left: auto;\n font-family: 'SFMono-Regular', Consolas, monospace;\n}\n\n/* Live dot — pulsing green indicator */\n.ls-live-dot {\n width: 8px;\n height: 8px;\n border-radius: 50%;\n background: #4caf50;\n animation: ls-pulse 1.5s ease-in-out infinite;\n}\n\n@keyframes ls-pulse {\n 0%,\n 100% {\n opacity: 1;\n transform: scale(1);\n }\n 50% {\n opacity: 0.5;\n transform: scale(0.8);\n }\n}\n\n/* Terminal view */\n.ls-terminal {\n max-height: 400px;\n overflow-y: auto;\n padding: 8px 12px;\n background: var(--center-channel-color-04, rgba(0, 0, 0, 0.04));\n font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;\n font-size: 12px;\n line-height: 1.6;\n}\n\n/* Status lines */\n.ls-status-line {\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.ls-tool-call {\n color: var(--center-channel-color, #3d3c40);\n}\n\n.ls-tool-name {\n color: var(--link-color, #2389d7);\n font-weight: 600;\n}\n\n.ls-tool-args {\n color: var(--center-channel-color-72, rgba(0, 0, 0, 0.72));\n}\n\n.ls-marker-ok {\n color: #4caf50;\n font-weight: 600;\n}\n\n.ls-marker-err {\n color: #f44336;\n font-weight: 600;\n}\n\n.ls-thinking {\n color: var(--center-channel-color-48, rgba(0, 0, 0, 0.48));\n}\n\n.ls-thinking-prefix {\n color: var(--center-channel-color-24, rgba(0, 0, 0, 0.24));\n}\n\n/* Children (sub-agents) */\n.ls-children {\n border-top: 1px solid var(--center-channel-color-08, rgba(0, 0, 0, 0.08));\n padding: 4px 0;\n}\n\n.ls-child {\n margin: 0 12px;\n border-left: 2px solid var(--center-channel-color-16, rgba(0, 0, 0, 0.16));\n padding-left: 8px;\n}\n\n.ls-child-header {\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 4px 0;\n font-size: 12px;\n}\n\n.ls-expand-icon {\n font-size: 10px;\n color: var(--center-channel-color-48, rgba(0, 0, 0, 0.48));\n width: 12px;\n}\n\n/* Footer */\n.ls-footer {\n padding: 4px 12px 8px;\n font-size: 11px;\n color: var(--center-channel-color-48, rgba(0, 0, 0, 0.48));\n font-family: 'SFMono-Regular', Consolas, monospace;\n}\n\n/* Loading state */\n.ls-loading {\n padding: 12px;\n color: var(--center-channel-color-48, rgba(0, 0, 0, 0.48));\n font-style: italic;\n}\n\n/* Scrollbar styling */\n.ls-terminal::-webkit-scrollbar {\n width: 6px;\n}\n\n.ls-terminal::-webkit-scrollbar-track {\n background: transparent;\n}\n\n.ls-terminal::-webkit-scrollbar-thumb {\n background: var(--center-channel-color-16, rgba(0, 0, 0, 0.16));\n border-radius: 3px;\n}\n\n.ls-terminal::-webkit-scrollbar-thumb:hover {\n background: var(--center-channel-color-32, rgba(0, 0, 0, 0.32));\n}\n\n/* ========= RHS Panel Styles ========= */\n\n.ls-rhs-panel {\n display: flex;\n flex-direction: column;\n height: 100%;\n overflow-y: auto;\n background: var(--center-channel-bg, #fff);\n}\n\n.ls-rhs-summary {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 12px 16px;\n border-bottom: 1px solid var(--center-channel-color-08, rgba(0, 0, 0, 0.08));\n font-size: 13px;\n font-weight: 600;\n color: var(--center-channel-color, #3d3c40);\n}\n\n.ls-rhs-count {\n display: flex;\n align-items: center;\n gap: 6px;\n}\n\n.ls-rhs-empty {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n padding: 48px 24px;\n text-align: center;\n}\n\n.ls-rhs-empty-icon {\n font-size: 32px;\n margin-bottom: 12px;\n}\n\n.ls-rhs-empty-text {\n color: var(--center-channel-color-48, rgba(0, 0, 0, 0.48));\n font-size: 13px;\n line-height: 1.5;\n}\n\n.ls-rhs-card {\n margin: 8px 12px;\n border: 1px solid var(--center-channel-color-08, rgba(0, 0, 0, 0.08));\n border-radius: 6px;\n overflow: hidden;\n}\n\n.ls-rhs-card.ls-status-active {\n border-color: rgba(76, 175, 80, 0.3);\n}\n\n.ls-rhs-card-header {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 8px 10px;\n background: var(--center-channel-color-04, rgba(0, 0, 0, 0.04));\n border-bottom: 1px solid var(--center-channel-color-08, rgba(0, 0, 0, 0.08));\n}\n\n.ls-rhs-card .ls-terminal {\n max-height: 250px;\n font-size: 11px;\n padding: 6px 10px;\n}\n\n.ls-rhs-card-footer {\n padding: 4px 10px 6px;\n font-size: 11px;\n color: var(--center-channel-color-48, rgba(0, 0, 0, 0.48));\n font-family: 'SFMono-Regular', Consolas, monospace;\n border-top: 1px solid var(--center-channel-color-08, rgba(0, 0, 0, 0.08));\n}\n\n.ls-rhs-children {\n padding: 4px 10px;\n border-top: 1px solid var(--center-channel-color-08, rgba(0, 0, 0, 0.08));\n}\n\n.ls-rhs-child {\n padding: 4px 0;\n}\n\n.ls-rhs-child-header {\n display: flex;\n align-items: center;\n gap: 6px;\n font-size: 11px;\n}\n\n.ls-rhs-section {\n margin-top: 4px;\n}\n\n.ls-rhs-section-title {\n padding: 8px 16px 4px;\n font-size: 11px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n color: var(--center-channel-color-48, rgba(0, 0, 0, 0.48));\n}\n\n/* ========= Floating Widget Styles ========= */\n\n.ls-widget-collapsed {\n position: fixed;\n z-index: 9999;\n cursor: pointer;\n display: flex;\n align-items: center;\n gap: 4px;\n padding: 8px;\n background: var(--center-channel-bg, #fff);\n border: 1px solid var(--center-channel-color-16, rgba(0, 0, 0, 0.16));\n border-radius: 50%;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);\n transition: transform 0.2s;\n}\n\n.ls-widget-collapsed:hover {\n transform: scale(1.1);\n}\n\n.ls-widget-collapsed .ls-live-dot {\n width: 12px;\n height: 12px;\n}\n\n.ls-widget-count {\n position: absolute;\n top: -4px;\n right: -4px;\n background: #f44336;\n color: #fff;\n font-size: 10px;\n font-weight: 700;\n width: 16px;\n height: 16px;\n border-radius: 50%;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.ls-widget-expanded {\n position: fixed;\n z-index: 9999;\n width: 320px;\n max-height: 300px;\n background: var(--center-channel-bg, #fff);\n border: 1px solid var(--center-channel-color-16, rgba(0, 0, 0, 0.16));\n border-radius: 8px;\n box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);\n display: flex;\n flex-direction: column;\n overflow: hidden;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n}\n\n.ls-widget-header {\n display: flex;\n align-items: center;\n padding: 8px 10px;\n background: var(--center-channel-color-04, rgba(0, 0, 0, 0.04));\n border-bottom: 1px solid var(--center-channel-color-08, rgba(0, 0, 0, 0.08));\n cursor: grab;\n user-select: none;\n}\n\n.ls-widget-header:active {\n cursor: grabbing;\n}\n\n.ls-widget-title {\n display: flex;\n align-items: center;\n gap: 6px;\n font-size: 12px;\n font-weight: 600;\n flex: 1;\n}\n\n.ls-widget-collapse-btn,\n.ls-widget-close-btn {\n background: none;\n border: none;\n cursor: pointer;\n font-size: 14px;\n color: var(--center-channel-color-56, rgba(0, 0, 0, 0.56));\n padding: 2px 6px;\n border-radius: 3px;\n}\n\n.ls-widget-collapse-btn:hover,\n.ls-widget-close-btn:hover {\n background: var(--center-channel-color-08, rgba(0, 0, 0, 0.08));\n}\n\n.ls-widget-body {\n padding: 8px;\n overflow-y: auto;\n flex: 1;\n}\n\n.ls-widget-session {\n margin-bottom: 4px;\n}\n\n.ls-widget-session-header {\n display: flex;\n align-items: center;\n gap: 6px;\n margin-bottom: 6px;\n}\n\n.ls-widget-lines {\n font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;\n font-size: 11px;\n line-height: 1.5;\n background: var(--center-channel-color-04, rgba(0, 0, 0, 0.04));\n border-radius: 4px;\n padding: 6px 8px;\n max-height: 150px;\n overflow-y: auto;\n}\n\n.ls-widget-empty {\n color: var(--center-channel-color-48, rgba(0, 0, 0, 0.48));\n font-size: 12px;\n text-align: center;\n padding: 12px;\n}\n\n.ls-widget-more {\n font-size: 11px;\n color: var(--center-channel-color-48, rgba(0, 0, 0, 0.48));\n text-align: center;\n padding: 4px;\n}\n",""]);const l=o},314(e){e.exports=function(e){var n=[];return n.toString=function(){return this.map(function(n){var t="",s=void 0!==n[5];return n[4]&&(t+="@supports (".concat(n[4],") {")),n[2]&&(t+="@media ".concat(n[2]," {")),s&&(t+="@layer".concat(n[5].length>0?" ".concat(n[5]):""," {")),t+=e(n),s&&(t+="}"),n[2]&&(t+="}"),n[4]&&(t+="}"),t}).join("")},n.i=function(e,t,s,a,r){"string"==typeof e&&(e=[[null,e,void 0]]);var o={};if(s)for(var l=0;l0?" ".concat(d[5]):""," {").concat(d[1],"}")),d[5]=r),t&&(d[2]?(d[1]="@media ".concat(d[2]," {").concat(d[1],"}"),d[2]=t):d[2]=t),a&&(d[4]?(d[1]="@supports (".concat(d[4],") {").concat(d[1],"}"),d[4]=a):d[4]="".concat(a)),n.push(d))}},n}},601(e){e.exports=function(e){return e[1]}},72(e){var n=[];function t(e){for(var t=-1,s=0;s0?" ".concat(t.layer):""," {")),s+=t.css,a&&(s+="}"),t.media&&(s+="}"),t.supports&&(s+="}");var r=t.sourceMap;r&&"undefined"!=typeof btoa&&(s+="\n/*# sourceMappingURL=data:application/json;base64,".concat(btoa(unescape(encodeURIComponent(JSON.stringify(r))))," */")),n.styleTagTransform(s,e,n.options)}(n,e,t)},remove:function(){!function(e){if(null===e.parentNode)return!1;e.parentNode.removeChild(e)}(n)}}}},113(e){e.exports=function(e,n){if(n.styleSheet)n.styleSheet.cssText=e;else{for(;n.firstChild;)n.removeChild(n.firstChild);n.appendChild(document.createTextNode(e))}}},883(n){n.exports=e}},t={};function s(e){var a=t[e];if(void 0!==a)return a.exports;var r=t[e]={id:e,exports:{}};return n[e](r,r.exports,s),r.exports}s.n=e=>{var n=e&&e.__esModule?()=>e.default:()=>e;return s.d(n,{a:n}),n},s.d=(e,n)=>{for(var t in n)s.o(n,t)&&!s.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:n[t]})},s.o=(e,n)=>Object.prototype.hasOwnProperty.call(e,n),s.nc=void 0;var a=s(883),r=s.n(a);const o=({line:e})=>{const n=e.match(/^(\S+?): (.+?)( \[OK\]| \[ERR\])?$/);if(n){const e=n[1],t=n[2],s=(n[3]||"").trim();return r().createElement("div",{className:"ls-status-line ls-tool-call"},r().createElement("span",{className:"ls-tool-name"},e,":"),r().createElement("span",{className:"ls-tool-args"}," ",t),s&&r().createElement("span",{className:"[OK]"===s?"ls-marker-ok":"ls-marker-err"}," ",s))}return r().createElement("div",{className:"ls-status-line ls-thinking"},r().createElement("span",{className:"ls-thinking-prefix"},"│"," "),r().createElement("span",{className:"ls-thinking-text"},e))},l=({lines:e,maxLines:n=30})=>{const t=(0,a.useRef)(null),[s,l]=(0,a.useState)(!1),i=(0,a.useRef)(null);(0,a.useEffect)(()=>{!s&&i.current&&i.current.scrollIntoView({behavior:"smooth"})},[e.length,s]);const c=e.slice(-n);return r().createElement("div",{className:"ls-terminal",ref:t,onScroll:()=>{if(!t.current)return;const{scrollTop:e,scrollHeight:n,clientHeight:s}=t.current;l(!(n-e-s<20))}},c.map((e,n)=>r().createElement(o,{key:n,line:e})),r().createElement("div",{ref:i}))};function i(e){e<0&&(e=0);const n=Math.floor(e/1e3),t=Math.floor(n/60),s=Math.floor(t/60);return s>0?`${s}h${t%60}m`:t>0?`${t}m${n%60}s`:`${n}s`}"undefined"!=typeof window&&(window.__livestatus_updates=window.__livestatus_updates||{},window.__livestatus_listeners=window.__livestatus_listeners||{});const c=({child:e})=>{const[n,t]=(0,a.useState)(!1);return r().createElement("div",{className:"ls-child"},r().createElement("div",{className:"ls-child-header",onClick:()=>t(!n),style:{cursor:"pointer"}},r().createElement("span",{className:"ls-expand-icon"},n?"▼":"▶"),r().createElement("span",{className:"ls-agent-badge ls-child-badge"},e.agent_id),r().createElement("span",{className:`ls-status-badge ls-status-${e.status}`},e.status.toUpperCase()),r().createElement("span",{className:"ls-elapsed"},i(e.elapsed_ms))),n&&r().createElement(l,{lines:e.lines,maxLines:15}))},d=({post:e,theme:n})=>{const[t,s]=(0,a.useState)(0),o={session_key:e.props.session_key||"",post_id:e.id,agent_id:e.props.agent_id||"unknown",status:e.props.status||"active",lines:e.props.final_lines||[],elapsed_ms:e.props.elapsed_ms||0,token_count:e.props.token_count||0,children:[],start_time_ms:e.props.start_time_ms||0},d=function(e,n){const[t,s]=(0,a.useState)(window.__livestatus_updates[e]||n);return(0,a.useEffect)(()=>{window.__livestatus_listeners[e]||(window.__livestatus_listeners[e]=[]);const n=e=>s(e);return window.__livestatus_listeners[e].push(n),window.__livestatus_updates[e]&&s(window.__livestatus_updates[e]),()=>{const t=window.__livestatus_listeners[e];if(t){const e=t.indexOf(n);e>=0&&t.splice(e,1)}}},[e]),t}(e.id,o),p="active"===(null==d?void 0:d.status);if((0,a.useEffect)(()=>{if(!p||!(null==d?void 0:d.start_time_ms))return;const e=setInterval(()=>{s(Date.now()-d.start_time_ms)},1e3);return()=>clearInterval(e)},[p,null==d?void 0:d.start_time_ms]),!d)return r().createElement("div",{className:"ls-post ls-loading"},"Loading status...");const u=p&&d.start_time_ms?i(t||Date.now()-d.start_time_ms):i(d.elapsed_ms),m=`ls-status-${d.status}`;return r().createElement("div",{className:`ls-post ${m}`},r().createElement("div",{className:"ls-header"},r().createElement("span",{className:"ls-agent-badge"},d.agent_id),p&&r().createElement("span",{className:"ls-live-dot"}),r().createElement("span",{className:`ls-status-badge ${m}`},d.status.toUpperCase()),r().createElement("span",{className:"ls-elapsed"},u)),r().createElement(l,{lines:d.lines,maxLines:30}),d.children&&d.children.length>0&&r().createElement("div",{className:"ls-children"},d.children.map((e,n)=>r().createElement(c,{key:e.session_key||n,child:e}))),!p&&d.token_count>0&&r().createElement("div",{className:"ls-footer"},(g=d.token_count)>=1e6?`${(g/1e6).toFixed(1)}M`:g>=1e3?`${(g/1e3).toFixed(1)}k`:String(g)," tokens"));var g};function p(e){e<0&&(e=0);const n=Math.floor(e/1e3),t=Math.floor(n/60),s=Math.floor(t/60);return s>0?`${s}h${t%60}m`:t>0?`${t}m${n%60}s`:`${n}s`}const u=({data:e})=>{const[n,t]=(0,a.useState)(0),s="active"===e.status;(0,a.useEffect)(()=>{if(!s||!e.start_time_ms)return;const n=setInterval(()=>{t(Date.now()-e.start_time_ms)},1e3);return()=>clearInterval(n)},[s,e.start_time_ms]);const o=s&&e.start_time_ms?p(n||Date.now()-e.start_time_ms):p(e.elapsed_ms),i=`ls-status-${e.status}`;return r().createElement("div",{className:`ls-rhs-card ${i}`},r().createElement("div",{className:"ls-rhs-card-header"},r().createElement("span",{className:"ls-agent-badge"},e.agent_id),s&&r().createElement("span",{className:"ls-live-dot"}),r().createElement("span",{className:`ls-status-badge ${i}`},e.status.toUpperCase()),r().createElement("span",{className:"ls-elapsed"},o)),r().createElement(l,{lines:e.lines,maxLines:20}),e.children&&e.children.length>0&&r().createElement("div",{className:"ls-rhs-children"},e.children.map((e,n)=>r().createElement("div",{key:e.session_key||n,className:"ls-rhs-child"},r().createElement("div",{className:"ls-rhs-child-header"},r().createElement("span",{className:"ls-agent-badge ls-child-badge"},e.agent_id),r().createElement("span",{className:`ls-status-badge ls-status-${e.status}`},e.status.toUpperCase()),r().createElement("span",{className:"ls-elapsed"},p(e.elapsed_ms)))))),e.token_count>0&&r().createElement("div",{className:"ls-rhs-card-footer"},(c=e.token_count)>=1e6?`${(c/1e6).toFixed(1)}M`:c>=1e3?`${(c/1e3).toFixed(1)}k`:String(c)," tokens"));var c},m=()=>{const e=function(){const[e,n]=(0,a.useState)(()=>Object.assign({},window.__livestatus_updates||{}));return(0,a.useEffect)(()=>{fetch("/plugins/com.openclaw.livestatus/api/v1/sessions",{credentials:"include"}).then(e=>e.json()).then(e=>{if(Array.isArray(e)){const t={};e.forEach(e=>{const n=e.post_id||e.session_key;t[n]=e}),window.__livestatus_updates=Object.assign(Object.assign({},t),window.__livestatus_updates),n(Object.assign({},window.__livestatus_updates))}}).catch(e=>console.warn("[LiveStatus] Failed to fetch initial sessions:",e));const e="__rhs_panel__";window.__livestatus_listeners[e]||(window.__livestatus_listeners[e]=[]);const t=()=>{n(Object.assign({},window.__livestatus_updates||{}))};window.__livestatus_listeners[e].push(t);const s=setInterval(()=>{const e=window.__livestatus_updates||{};n(n=>{if(Object.keys(n).sort().join(",")!==Object.keys(e).sort().join(","))return Object.assign({},e);for(const t of Object.keys(e))if(e[t]!==n[t])return Object.assign({},e);return n})},2e3);return()=>{clearInterval(s);const n=window.__livestatus_listeners[e];if(n){const e=n.indexOf(t);e>=0&&n.splice(e,1)}}},[]),e}(),n=Object.values(e).sort((e,n)=>"active"===e.status&&"active"!==n.status?-1:"active"===n.status&&"active"!==e.status?1:(n.start_time_ms||0)-(e.start_time_ms||0)),t=n.filter(e=>"active"===e.status),s=n.filter(e=>"active"!==e.status);return r().createElement("div",{className:"ls-rhs-panel"},r().createElement("div",{className:"ls-rhs-summary"},r().createElement("span",{className:"ls-rhs-count"},t.length>0?r().createElement(r().Fragment,null,r().createElement("span",{className:"ls-live-dot"}),t.length," active"):"No active sessions")),0===t.length&&0===s.length&&r().createElement("div",{className:"ls-rhs-empty"},r().createElement("div",{className:"ls-rhs-empty-icon"},"⚙️"),r().createElement("div",{className:"ls-rhs-empty-text"},"No agent activity yet.",r().createElement("br",null),"Status will appear here when an agent starts working.")),t.map(e=>r().createElement(u,{key:e.post_id||e.session_key,data:e})),s.length>0&&r().createElement("div",{className:"ls-rhs-section"},r().createElement("div",{className:"ls-rhs-section-title"},"Recent"),s.slice(0,5).map(e=>r().createElement(u,{key:e.post_id||e.session_key,data:e}))))},g="livestatus_widget_pos",f=()=>{const[e,n]=(0,a.useState)({}),[t,s]=(0,a.useState)(!0),[l,i]=(0,a.useState)(!1),[c,d]=(0,a.useState)(()=>{try{const e=localStorage.getItem(g);if(e)return JSON.parse(e)}catch(e){}return{right:20,bottom:80}}),[p,u]=(0,a.useState)(!1),m=(0,a.useRef)(null),f=(0,a.useRef)(null),h=(0,a.useRef)(null);(0,a.useEffect)(()=>{const e="__floating_widget__";window.__livestatus_listeners[e]||(window.__livestatus_listeners[e]=[]);const t=()=>{n(Object.assign({},window.__livestatus_updates||{}))};window.__livestatus_listeners[e].push(t);const s=setInterval(()=>{n(e=>{const n=window.__livestatus_updates||{};if(Object.keys(e).sort().join(",")!==Object.keys(n).sort().join(","))return Object.assign({},n);for(const t of Object.keys(n))if(n[t]!==e[t])return Object.assign({},n);return e})},2e3);return()=>{clearInterval(s);const n=window.__livestatus_listeners[e];if(n){const e=n.indexOf(t);e>=0&&n.splice(e,1)}}},[]),(0,a.useEffect)(()=>{const n=Object.values(e);n.some(e=>"active"===e.status)?(f.current&&(clearTimeout(f.current),f.current=null),i(!0)):l&&n.length>0&&(f.current||(f.current=window.setTimeout(()=>{i(!1),s(!0),f.current=null},5e3)))},[e,l]),(0,a.useEffect)(()=>{try{localStorage.setItem(g,JSON.stringify(c))}catch(e){}},[c]);const v=(0,a.useCallback)(e=>{e.preventDefault(),m.current={startX:e.clientX,startY:e.clientY,startRight:c.right,startBottom:c.bottom},u(!0)},[c]);if((0,a.useEffect)(()=>{if(!p)return;const e=e=>{if(!m.current)return;const n=m.current.startX-e.clientX,t=m.current.startY-e.clientY;d({right:Math.max(0,m.current.startRight+n),bottom:Math.max(0,m.current.startBottom+t)})},n=()=>{u(!1),m.current=null};return document.addEventListener("mousemove",e),document.addEventListener("mouseup",n),()=>{document.removeEventListener("mousemove",e),document.removeEventListener("mouseup",n)}},[p]),!l)return null;const _=Object.values(e),x=_.filter(e=>"active"===e.status),b=x[0]||_[_.length-1],w=x.length;return t?r().createElement("div",{ref:h,className:"ls-widget-collapsed",style:{right:c.right,bottom:c.bottom},onClick:()=>s(!1),title:`${w} active agent session${1!==w?"s":""}`},r().createElement("span",{className:"ls-live-dot"}),w>0&&r().createElement("span",{className:"ls-widget-count"},w)):r().createElement("div",{ref:h,className:"ls-widget-expanded",style:{right:c.right,bottom:c.bottom}},r().createElement("div",{className:"ls-widget-header",onMouseDown:v},r().createElement("span",{className:"ls-widget-title"},w>0&&r().createElement("span",{className:"ls-live-dot"}),"Agent Status",w>0&&` (${w})`),r().createElement("button",{className:"ls-widget-collapse-btn",onClick:()=>s(!0)},"_"),r().createElement("button",{className:"ls-widget-close-btn",onClick:()=>i(!1)},"×")),r().createElement("div",{className:"ls-widget-body"},b?r().createElement("div",{className:"ls-widget-session"},r().createElement("div",{className:"ls-widget-session-header"},r().createElement("span",{className:"ls-agent-badge"},b.agent_id),r().createElement("span",{className:`ls-status-badge ls-status-${b.status}`},b.status.toUpperCase())),r().createElement("div",{className:"ls-widget-lines"},b.lines.slice(-5).map((e,n)=>r().createElement(o,{key:n,line:e})))):r().createElement("div",{className:"ls-widget-empty"},"No sessions"),x.length>1&&r().createElement("div",{className:"ls-widget-more"},"+",x.length-1," more active")))};var h=s(72),v=s.n(h),_=s(825),x=s.n(_),b=s(659),w=s.n(b),y=s(56),E=s.n(y),k=s(540),N=s.n(k),S=s(113),j=s.n(S),M=s(999),C={};C.styleTagTransform=j(),C.setAttributes=E(),C.insert=w().bind(null,"head"),C.domAPI=x(),C.insertStyleElement=N(),v()(M.A,C),M.A&&M.A.locals&&M.A.locals;const O="com.openclaw.livestatus",R=`custom_${O}_update`;return"undefined"!=typeof window&&(window.__livestatus_updates=window.__livestatus_updates||{},window.__livestatus_listeners=window.__livestatus_listeners||{}),window.registerPlugin(O,new class{constructor(){this.postTypeComponentId=null}initialize(e,n){this.postTypeComponentId=e.registerPostTypeComponent("custom_livestatus",d);const t=e.registerRightHandSidebarComponent({component:m,title:"Agent Status"}).toggleRHSPlugin;e.registerChannelHeaderButtonAction({icon:r().createElement("i",{className:"icon icon-cog-outline",style:{fontSize:"18px"}}),action:()=>{n.dispatch(t)},dropdownText:"Agent Status",tooltipText:"Toggle Agent Status panel"}),e.registerRootComponent(f),e.registerWebSocketEventHandler(R,e=>{const n=e.data;if(!n||!n.post_id)return;const t={session_key:n.session_key,post_id:n.post_id,agent_id:n.agent_id,status:n.status,lines:n.lines||[],elapsed_ms:n.elapsed_ms||0,token_count:n.token_count||0,children:n.children||[],start_time_ms:n.start_time_ms||0};window.__livestatus_updates[n.post_id]=t;const s=window.__livestatus_listeners[n.post_id];s&&s.forEach(e=>e(t));const a=window.__livestatus_listeners.__rhs_panel__;a&&a.forEach(e=>e(t));const r=window.__livestatus_listeners.__floating_widget__;r&&r.forEach(e=>e(t))})}uninitialize(){}}),{}})()); \ No newline at end of file diff --git a/plugin/webapp/src/components/floating_widget.tsx b/plugin/webapp/src/components/floating_widget.tsx new file mode 100644 index 0000000..112d399 --- /dev/null +++ b/plugin/webapp/src/components/floating_widget.tsx @@ -0,0 +1,197 @@ +import React, { useState, useEffect, useRef, useCallback } from 'react'; +import { LiveStatusData } from '../types'; +import StatusLine from './status_line'; + +const STORAGE_KEY = 'livestatus_widget_pos'; +const AUTO_HIDE_DELAY = 5000; // 5s after all sessions complete + +const FloatingWidget: React.FC = () => { + const [sessions, setSessions] = useState>({}); + const [collapsed, setCollapsed] = useState(true); + const [visible, setVisible] = useState(false); + const [position, setPosition] = useState(() => { + try { + const saved = localStorage.getItem(STORAGE_KEY); + if (saved) return JSON.parse(saved); + } catch {} + return { right: 20, bottom: 80 }; + }); + const [dragging, setDragging] = useState(false); + const dragRef = useRef<{ startX: number; startY: number; startRight: number; startBottom: number } | null>(null); + const hideTimerRef = useRef(null); + const widgetRef = useRef(null); + + // Subscribe to updates (same pattern as RHS panel) + useEffect(() => { + const key = '__floating_widget__'; + if (!window.__livestatus_listeners[key]) { + window.__livestatus_listeners[key] = []; + } + + const listener = () => { + setSessions({ ...(window.__livestatus_updates || {}) }); + }; + + window.__livestatus_listeners[key].push(listener); + + // Polling fallback + const interval = setInterval(() => { + setSessions(prev => { + const current = window.__livestatus_updates || {}; + const prevKeys = Object.keys(prev).sort().join(','); + const currKeys = Object.keys(current).sort().join(','); + if (prevKeys !== currKeys) return { ...current }; + for (const k of Object.keys(current)) { + if (current[k] !== prev[k]) return { ...current }; + } + return prev; + }); + }, 2000); + + return () => { + clearInterval(interval); + const listeners = window.__livestatus_listeners[key]; + if (listeners) { + const idx = listeners.indexOf(listener); + if (idx >= 0) listeners.splice(idx, 1); + } + }; + }, []); + + // Auto-show/hide logic + useEffect(() => { + const entries = Object.values(sessions); + const hasActive = entries.some(s => s.status === 'active'); + + if (hasActive) { + // Clear any pending hide timer + if (hideTimerRef.current) { + clearTimeout(hideTimerRef.current); + hideTimerRef.current = null; + } + setVisible(true); + } else if (visible && entries.length > 0) { + // All done - hide after delay + if (!hideTimerRef.current) { + hideTimerRef.current = window.setTimeout(() => { + setVisible(false); + setCollapsed(true); + hideTimerRef.current = null; + }, AUTO_HIDE_DELAY); + } + } + }, [sessions, visible]); + + // Save position to localStorage + useEffect(() => { + try { + localStorage.setItem(STORAGE_KEY, JSON.stringify(position)); + } catch {} + }, [position]); + + // Drag handlers + const onMouseDown = useCallback((e: React.MouseEvent) => { + e.preventDefault(); + dragRef.current = { + startX: e.clientX, + startY: e.clientY, + startRight: position.right, + startBottom: position.bottom, + }; + setDragging(true); + }, [position]); + + useEffect(() => { + if (!dragging) return; + + const onMouseMove = (e: MouseEvent) => { + if (!dragRef.current) return; + const dx = dragRef.current.startX - e.clientX; + const dy = dragRef.current.startY - e.clientY; + setPosition({ + right: Math.max(0, dragRef.current.startRight + dx), + bottom: Math.max(0, dragRef.current.startBottom + dy), + }); + }; + + const onMouseUp = () => { + setDragging(false); + dragRef.current = null; + }; + + document.addEventListener('mousemove', onMouseMove); + document.addEventListener('mouseup', onMouseUp); + return () => { + document.removeEventListener('mousemove', onMouseMove); + document.removeEventListener('mouseup', onMouseUp); + }; + }, [dragging]); + + if (!visible) return null; + + const entries = Object.values(sessions); + const activeSessions = entries.filter(s => s.status === 'active'); + const displaySession = activeSessions[0] || entries[entries.length - 1]; + const activeCount = activeSessions.length; + + // Collapsed = pulsing dot with count + if (collapsed) { + return ( +
setCollapsed(false)} + title={`${activeCount} active agent session${activeCount !== 1 ? 's' : ''}`} + > + + {activeCount > 0 && {activeCount}} +
+ ); + } + + // Expanded view + return ( +
+
+ + {activeCount > 0 && } + Agent Status + {activeCount > 0 && ` (${activeCount})`} + + + +
+
+ {displaySession ? ( +
+
+ {displaySession.agent_id} + + {displaySession.status.toUpperCase()} + +
+
+ {displaySession.lines.slice(-5).map((line, i) => ( + + ))} +
+
+ ) : ( +
No sessions
+ )} + {activeSessions.length > 1 && ( +
+ +{activeSessions.length - 1} more active +
+ )} +
+
+ ); +}; + +export default FloatingWidget; diff --git a/plugin/webapp/src/components/rhs_panel.tsx b/plugin/webapp/src/components/rhs_panel.tsx index ee97e3e..77e381c 100644 --- a/plugin/webapp/src/components/rhs_panel.tsx +++ b/plugin/webapp/src/components/rhs_panel.tsx @@ -12,6 +12,30 @@ function useAllStatusUpdates(): Record { }); useEffect(() => { + // Initial fetch of existing sessions from the plugin API. + // Uses credentials: 'include' so Mattermost session cookies are forwarded. + // The server allows unauthenticated GET /sessions for MM-authenticated users. + fetch('/plugins/com.openclaw.livestatus/api/v1/sessions', { + credentials: 'include', + }) + .then((res) => res.json()) + .then((fetchedSessions: LiveStatusData[]) => { + if (Array.isArray(fetchedSessions)) { + const updates: Record = {}; + fetchedSessions.forEach((s) => { + const key = s.post_id || s.session_key; + updates[key] = s; + }); + // Merge: WebSocket data (already in window) takes precedence over fetched data + window.__livestatus_updates = { + ...updates, + ...window.__livestatus_updates, + }; + setSessions({ ...window.__livestatus_updates }); + } + }) + .catch((err) => console.warn('[LiveStatus] Failed to fetch initial sessions:', err)); + // Register a global listener that catches all updates const globalKey = '__rhs_panel__'; if (!window.__livestatus_listeners[globalKey]) { diff --git a/plugin/webapp/src/index.tsx b/plugin/webapp/src/index.tsx index e4be6a9..ccc40c3 100644 --- a/plugin/webapp/src/index.tsx +++ b/plugin/webapp/src/index.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { PluginRegistry, WebSocketPayload, LiveStatusData } from './types'; import LiveStatusPost from './components/live_status_post'; import RHSPanel from './components/rhs_panel'; +import FloatingWidget from './components/floating_widget'; import './styles/live_status.css'; const PLUGIN_ID = 'com.openclaw.livestatus'; @@ -37,6 +38,9 @@ class LiveStatusPlugin { tooltipText: 'Toggle Agent Status panel', }); + // Register floating widget as root component (always rendered) + registry.registerRootComponent(FloatingWidget); + // Register WebSocket event handler registry.registerWebSocketEventHandler(WS_EVENT, (msg: any) => { const data = msg.data as WebSocketPayload; @@ -68,6 +72,12 @@ class LiveStatusPlugin { if (rhsListeners) { rhsListeners.forEach((fn) => fn(update)); } + + // Notify floating widget listener + const widgetListeners = window.__livestatus_listeners['__floating_widget__']; + if (widgetListeners) { + widgetListeners.forEach((fn) => fn(update)); + } }); } diff --git a/plugin/webapp/src/styles/live_status.css b/plugin/webapp/src/styles/live_status.css index 3fde6ca..a70138a 100644 --- a/plugin/webapp/src/styles/live_status.css +++ b/plugin/webapp/src/styles/live_status.css @@ -305,3 +305,141 @@ letter-spacing: 0.5px; color: var(--center-channel-color-48, rgba(0, 0, 0, 0.48)); } + +/* ========= Floating Widget Styles ========= */ + +.ls-widget-collapsed { + position: fixed; + z-index: 9999; + cursor: pointer; + display: flex; + align-items: center; + gap: 4px; + padding: 8px; + background: var(--center-channel-bg, #fff); + border: 1px solid var(--center-channel-color-16, rgba(0, 0, 0, 0.16)); + border-radius: 50%; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); + transition: transform 0.2s; +} + +.ls-widget-collapsed:hover { + transform: scale(1.1); +} + +.ls-widget-collapsed .ls-live-dot { + width: 12px; + height: 12px; +} + +.ls-widget-count { + position: absolute; + top: -4px; + right: -4px; + background: #f44336; + color: #fff; + font-size: 10px; + font-weight: 700; + width: 16px; + height: 16px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; +} + +.ls-widget-expanded { + position: fixed; + z-index: 9999; + width: 320px; + max-height: 300px; + background: var(--center-channel-bg, #fff); + border: 1px solid var(--center-channel-color-16, rgba(0, 0, 0, 0.16)); + border-radius: 8px; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15); + display: flex; + flex-direction: column; + overflow: hidden; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; +} + +.ls-widget-header { + display: flex; + align-items: center; + padding: 8px 10px; + background: var(--center-channel-color-04, rgba(0, 0, 0, 0.04)); + border-bottom: 1px solid var(--center-channel-color-08, rgba(0, 0, 0, 0.08)); + cursor: grab; + user-select: none; +} + +.ls-widget-header:active { + cursor: grabbing; +} + +.ls-widget-title { + display: flex; + align-items: center; + gap: 6px; + font-size: 12px; + font-weight: 600; + flex: 1; +} + +.ls-widget-collapse-btn, +.ls-widget-close-btn { + background: none; + border: none; + cursor: pointer; + font-size: 14px; + color: var(--center-channel-color-56, rgba(0, 0, 0, 0.56)); + padding: 2px 6px; + border-radius: 3px; +} + +.ls-widget-collapse-btn:hover, +.ls-widget-close-btn:hover { + background: var(--center-channel-color-08, rgba(0, 0, 0, 0.08)); +} + +.ls-widget-body { + padding: 8px; + overflow-y: auto; + flex: 1; +} + +.ls-widget-session { + margin-bottom: 4px; +} + +.ls-widget-session-header { + display: flex; + align-items: center; + gap: 6px; + margin-bottom: 6px; +} + +.ls-widget-lines { + font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace; + font-size: 11px; + line-height: 1.5; + background: var(--center-channel-color-04, rgba(0, 0, 0, 0.04)); + border-radius: 4px; + padding: 6px 8px; + max-height: 150px; + overflow-y: auto; +} + +.ls-widget-empty { + color: var(--center-channel-color-48, rgba(0, 0, 0, 0.48)); + font-size: 12px; + text-align: center; + padding: 12px; +} + +.ls-widget-more { + font-size: 11px; + color: var(--center-channel-color-48, rgba(0, 0, 0, 0.48)); + text-align: center; + padding: 4px; +} diff --git a/plugin/webapp/src/types.ts b/plugin/webapp/src/types.ts index c514080..43892a2 100644 --- a/plugin/webapp/src/types.ts +++ b/plugin/webapp/src/types.ts @@ -47,6 +47,7 @@ export interface PluginRegistry { dropdownText: string; tooltipText: string; }): string; + registerRootComponent(component: React.ComponentType): string; } export interface Post { diff --git a/src/status-watcher.js b/src/status-watcher.js index 058dd01..2f953a3 100644 --- a/src/status-watcher.js +++ b/src/status-watcher.js @@ -107,16 +107,27 @@ class StatusWatcher extends EventEmitter { _startFilePoll(sessionKey, state) { var self = this; var pollInterval = 500; // 500ms poll + var pollCount = 0; state._filePollTimer = setInterval(function () { try { var stat = fs.statSync(state.transcriptFile); + pollCount++; if (stat.size > state.lastOffset) { + if (self.logger) { + self.logger.info( + { sessionKey, fileSize: stat.size, lastOffset: state.lastOffset, delta: stat.size - state.lastOffset, pollCount }, + 'File poll: new data detected — reading', + ); + } self._readFile(sessionKey, state); } } catch (_e) { // File might not exist yet or was deleted } }, pollInterval); + if (self.logger) { + self.logger.info({ sessionKey, pollInterval }, 'File poll timer started'); + } } /** @@ -214,6 +225,9 @@ class StatusWatcher extends EventEmitter { const state = this.sessions.get(sessionKey); if (!state) return; + if (this.logger) { + this.logger.info({ sessionKey, fullPath: path.basename(fullPath) }, 'fs.watch: file change detected'); + } this._readFile(sessionKey, state); }