package main import ( "encoding/json" "fmt" "os" "os/exec" "path/filepath" "strings" ) // AgentState matches the TypeScript interface from dashboard.ts type AgentState struct { Session string `json:"session"` Machine string `json:"machine"` AgentType string `json:"agent_type"` Model string `json:"model"` Status string `json:"status"` StartedAt *string `json:"started_at"` CompletedAt *string `json:"completed_at"` DurationSeconds int `json:"duration_seconds"` ToolCalls int `json:"tool_calls"` CurrentTask string `json:"current_task"` LastTool *string `json:"last_tool"` LastToolAt *string `json:"last_tool_at"` ConnectionType string `json:"connection_type"` ConnectionName string `json:"connection_name"` Blocked bool `json:"blocked"` BlockedPrompt *string `json:"blocked_prompt"` CostEstimate float64 `json:"cost_estimate"` } func (a AgentState) StatusSymbol() string { switch a.Status { case "running": return "●" case "completed": return "✓" case "error": return "✕" case "blocked": return "⚠" default: return "○" } } func (a AgentState) DurationStr() string { d := a.DurationSeconds if d == 0 { return "" } if d < 60 { return fmt.Sprintf("%ds", d) } if d < 3600 { return fmt.Sprintf("%dm%ds", d/60, d%60) } return fmt.Sprintf("%dh%dm", d/3600, (d%3600)/60) } func (a AgentState) ShortModel() string { parts := strings.Split(a.Model, "/") if len(parts) == 2 { s := parts[1] if len(s) > 16 { s = s[:14] + ".." } return s } if len(a.Model) > 16 { return a.Model[:14] + ".." } return a.Model } func (a AgentState) MachineShort() string { s := a.Machine if len(s) > 12 { s = s[:10] + ".." } return s } // IsLocal returns true if this agent is on the same machine func (a AgentState) IsLocal() bool { return a.Machine == localHostname } var localHostname string // readLocalStates reads all JSON files from the given directory func readLocalStates(dir string) ([]AgentState, error) { entries, err := os.ReadDir(dir) if err != nil { if os.IsNotExist(err) { return nil, nil } return nil, fmt.Errorf("reading dir %s: %w", dir, err) } var states []AgentState for _, entry := range entries { if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".json") { continue } s, err := readStateFile(filepath.Join(dir, entry.Name())) if err == nil { states = append(states, s) } } return states, nil } func readStateFile(path string) (AgentState, error) { data, err := os.ReadFile(path) if err != nil { return AgentState{}, err } var s AgentState if err := json.Unmarshal(data, &s); err != nil { return AgentState{}, err } return s, nil } // readRemoteStates SSHes into a machine and reads state files func readRemoteStates(cfg RemoteConfig) ([]AgentState, error) { hostPort := cfg.Host if cfg.Port > 0 && cfg.Port != 22 { hostPort = fmt.Sprintf("%s:%d", cfg.Host, cfg.Port) } user := cfg.User if user == "" { user = "sam" } remotePath := cfg.Path if remotePath == "" { home, _ := os.UserHomeDir() remotePath = filepath.Join(home, ".pi", "agent", "dashboard") } // SSH in, list JSON files, cat each one cmd := exec.Command("ssh", "-o", "ConnectTimeout=5", "-o", "BatchMode=yes", fmt.Sprintf("%s@%s", user, hostPort), fmt.Sprintf("cat %s/*.json 2>/dev/null || true", remotePath), ) output, err := cmd.Output() if err != nil { return nil, fmt.Errorf("ssh %s: %w", cfg.Host, err) } // Multiple JSON objects concatenated — split by newline boundaries // and parse each line individually lines := strings.Split(strings.TrimSpace(string(output)), "\n") var states []AgentState for _, line := range lines { line = strings.TrimSpace(line) if line == "" { continue } var s AgentState if err := json.Unmarshal([]byte(line), &s); err != nil { continue // skip malformed lines } states = append(states, s) } return states, nil }