Merge polecat/rictus: fix mail priority flag (gt-kspu)
This commit is contained in:
@@ -19,7 +19,8 @@ import (
|
||||
var (
|
||||
mailSubject string
|
||||
mailBody string
|
||||
mailPriority string
|
||||
mailPriority int
|
||||
mailUrgent bool
|
||||
mailType string
|
||||
mailReplyTo string
|
||||
mailNotify bool
|
||||
@@ -61,14 +62,21 @@ Message types:
|
||||
notification - Informational (default)
|
||||
reply - Response to message
|
||||
|
||||
Priority levels:
|
||||
low, normal (default), high, urgent
|
||||
Priority levels (compatible with bd mail send):
|
||||
0 - urgent/critical
|
||||
1 - high
|
||||
2 - normal (default)
|
||||
3 - low
|
||||
4 - backlog
|
||||
|
||||
Use --urgent as shortcut for --priority 0.
|
||||
|
||||
Examples:
|
||||
gt mail send gastown/Toast -s "Status check" -m "How's that bug fix going?"
|
||||
gt mail send mayor/ -s "Work complete" -m "Finished gt-abc"
|
||||
gt mail send gastown/ -s "All hands" -m "Swarm starting" --notify
|
||||
gt mail send gastown/Toast -s "Task" -m "Fix bug" --type task --priority high
|
||||
gt mail send gastown/Toast -s "Task" -m "Fix bug" --type task --priority 1
|
||||
gt mail send gastown/Toast -s "Urgent" -m "Help!" --urgent
|
||||
gt mail send mayor/ -s "Re: Status" -m "Done" --reply-to msg-abc123`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: runMailSend,
|
||||
@@ -167,7 +175,8 @@ func init() {
|
||||
// Send flags
|
||||
mailSendCmd.Flags().StringVarP(&mailSubject, "subject", "s", "", "Message subject (required)")
|
||||
mailSendCmd.Flags().StringVarP(&mailBody, "message", "m", "", "Message body")
|
||||
mailSendCmd.Flags().StringVar(&mailPriority, "priority", "normal", "Message priority (low, normal, high, urgent)")
|
||||
mailSendCmd.Flags().IntVar(&mailPriority, "priority", 2, "Message priority (0=urgent, 1=high, 2=normal, 3=low, 4=backlog)")
|
||||
mailSendCmd.Flags().BoolVar(&mailUrgent, "urgent", false, "Set priority=0 (urgent)")
|
||||
mailSendCmd.Flags().StringVar(&mailType, "type", "notification", "Message type (task, scavenge, notification, reply)")
|
||||
mailSendCmd.Flags().StringVar(&mailReplyTo, "reply-to", "", "Message ID this is replying to")
|
||||
mailSendCmd.Flags().BoolVarP(&mailNotify, "notify", "n", false, "Send tmux notification to recipient")
|
||||
@@ -226,8 +235,12 @@ func runMailSend(cmd *cobra.Command, args []string) error {
|
||||
Body: mailBody,
|
||||
}
|
||||
|
||||
// Set priority
|
||||
msg.Priority = mail.ParsePriority(mailPriority)
|
||||
// Set priority (--urgent overrides --priority)
|
||||
if mailUrgent {
|
||||
msg.Priority = mail.PriorityUrgent
|
||||
} else {
|
||||
msg.Priority = mail.PriorityFromInt(mailPriority)
|
||||
}
|
||||
if mailNotify && msg.Priority == mail.PriorityNormal {
|
||||
msg.Priority = mail.PriorityHigh
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steveyegge/gastown/internal/config"
|
||||
@@ -169,9 +170,29 @@ Examples:
|
||||
RunE: runPolecatSync,
|
||||
}
|
||||
|
||||
var polecatStatusCmd = &cobra.Command{
|
||||
Use: "status <rig>/<polecat>",
|
||||
Short: "Show detailed status for a polecat",
|
||||
Long: `Show detailed status for a polecat.
|
||||
|
||||
Displays comprehensive information including:
|
||||
- Current lifecycle state (working, done, stuck, idle)
|
||||
- Assigned issue (if any)
|
||||
- Session status (running/stopped, attached/detached)
|
||||
- Session creation time
|
||||
- Last activity time
|
||||
|
||||
Examples:
|
||||
gt polecat status gastown/Toast
|
||||
gt polecat status gastown/Toast --json`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: runPolecatStatus,
|
||||
}
|
||||
|
||||
var (
|
||||
polecatSyncAll bool
|
||||
polecatSyncFromMain bool
|
||||
polecatStatusJSON bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -186,6 +207,9 @@ func init() {
|
||||
polecatSyncCmd.Flags().BoolVar(&polecatSyncAll, "all", false, "Sync all polecats in the rig")
|
||||
polecatSyncCmd.Flags().BoolVar(&polecatSyncFromMain, "from-main", false, "Pull only, no push")
|
||||
|
||||
// Status flags
|
||||
polecatStatusCmd.Flags().BoolVar(&polecatStatusJSON, "json", false, "Output as JSON")
|
||||
|
||||
// Add subcommands
|
||||
polecatCmd.AddCommand(polecatListCmd)
|
||||
polecatCmd.AddCommand(polecatAddCmd)
|
||||
@@ -195,6 +219,7 @@ func init() {
|
||||
polecatCmd.AddCommand(polecatDoneCmd)
|
||||
polecatCmd.AddCommand(polecatResetCmd)
|
||||
polecatCmd.AddCommand(polecatSyncCmd)
|
||||
polecatCmd.AddCommand(polecatStatusCmd)
|
||||
|
||||
rootCmd.AddCommand(polecatCmd)
|
||||
}
|
||||
@@ -577,3 +602,152 @@ func runPolecatSync(cmd *cobra.Command, args []string) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// PolecatStatus represents detailed polecat status for JSON output.
|
||||
type PolecatStatus struct {
|
||||
Rig string `json:"rig"`
|
||||
Name string `json:"name"`
|
||||
State polecat.State `json:"state"`
|
||||
Issue string `json:"issue,omitempty"`
|
||||
ClonePath string `json:"clone_path"`
|
||||
Branch string `json:"branch"`
|
||||
SessionRunning bool `json:"session_running"`
|
||||
SessionID string `json:"session_id,omitempty"`
|
||||
Attached bool `json:"attached,omitempty"`
|
||||
Windows int `json:"windows,omitempty"`
|
||||
CreatedAt string `json:"created_at,omitempty"`
|
||||
LastActivity string `json:"last_activity,omitempty"`
|
||||
}
|
||||
|
||||
func runPolecatStatus(cmd *cobra.Command, args []string) error {
|
||||
rigName, polecatName, err := parseAddress(args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mgr, r, err := getPolecatManager(rigName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get polecat info
|
||||
p, err := mgr.Get(polecatName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("polecat '%s' not found in rig '%s'", polecatName, rigName)
|
||||
}
|
||||
|
||||
// Get session info
|
||||
t := tmux.NewTmux()
|
||||
sessMgr := session.NewManager(t, r)
|
||||
sessInfo, err := sessMgr.Status(polecatName)
|
||||
if err != nil {
|
||||
// Non-fatal - continue without session info
|
||||
sessInfo = &session.Info{
|
||||
Polecat: polecatName,
|
||||
Running: false,
|
||||
}
|
||||
}
|
||||
|
||||
// JSON output
|
||||
if polecatStatusJSON {
|
||||
status := PolecatStatus{
|
||||
Rig: rigName,
|
||||
Name: polecatName,
|
||||
State: p.State,
|
||||
Issue: p.Issue,
|
||||
ClonePath: p.ClonePath,
|
||||
Branch: p.Branch,
|
||||
SessionRunning: sessInfo.Running,
|
||||
SessionID: sessInfo.SessionID,
|
||||
Attached: sessInfo.Attached,
|
||||
Windows: sessInfo.Windows,
|
||||
}
|
||||
if !sessInfo.Created.IsZero() {
|
||||
status.CreatedAt = sessInfo.Created.Format("2006-01-02 15:04:05")
|
||||
}
|
||||
if !sessInfo.LastActivity.IsZero() {
|
||||
status.LastActivity = sessInfo.LastActivity.Format("2006-01-02 15:04:05")
|
||||
}
|
||||
enc := json.NewEncoder(os.Stdout)
|
||||
enc.SetIndent("", " ")
|
||||
return enc.Encode(status)
|
||||
}
|
||||
|
||||
// Human-readable output
|
||||
fmt.Printf("%s\n\n", style.Bold.Render(fmt.Sprintf("Polecat: %s/%s", rigName, polecatName)))
|
||||
|
||||
// State with color
|
||||
stateStr := string(p.State)
|
||||
switch p.State {
|
||||
case polecat.StateWorking:
|
||||
stateStr = style.Info.Render(stateStr)
|
||||
case polecat.StateStuck:
|
||||
stateStr = style.Warning.Render(stateStr)
|
||||
case polecat.StateDone:
|
||||
stateStr = style.Success.Render(stateStr)
|
||||
default:
|
||||
stateStr = style.Dim.Render(stateStr)
|
||||
}
|
||||
fmt.Printf(" State: %s\n", stateStr)
|
||||
|
||||
// Issue
|
||||
if p.Issue != "" {
|
||||
fmt.Printf(" Issue: %s\n", p.Issue)
|
||||
} else {
|
||||
fmt.Printf(" Issue: %s\n", style.Dim.Render("(none)"))
|
||||
}
|
||||
|
||||
// Clone path and branch
|
||||
fmt.Printf(" Clone: %s\n", style.Dim.Render(p.ClonePath))
|
||||
fmt.Printf(" Branch: %s\n", style.Dim.Render(p.Branch))
|
||||
|
||||
// Session info
|
||||
fmt.Println()
|
||||
fmt.Printf("%s\n", style.Bold.Render("Session"))
|
||||
|
||||
if sessInfo.Running {
|
||||
fmt.Printf(" Status: %s\n", style.Success.Render("running"))
|
||||
fmt.Printf(" Session ID: %s\n", style.Dim.Render(sessInfo.SessionID))
|
||||
|
||||
if sessInfo.Attached {
|
||||
fmt.Printf(" Attached: %s\n", style.Info.Render("yes"))
|
||||
} else {
|
||||
fmt.Printf(" Attached: %s\n", style.Dim.Render("no"))
|
||||
}
|
||||
|
||||
if sessInfo.Windows > 0 {
|
||||
fmt.Printf(" Windows: %d\n", sessInfo.Windows)
|
||||
}
|
||||
|
||||
if !sessInfo.Created.IsZero() {
|
||||
fmt.Printf(" Created: %s\n", sessInfo.Created.Format("2006-01-02 15:04:05"))
|
||||
}
|
||||
|
||||
if !sessInfo.LastActivity.IsZero() {
|
||||
// Show relative time for activity
|
||||
ago := formatActivityTime(sessInfo.LastActivity)
|
||||
fmt.Printf(" Last Activity: %s (%s)\n",
|
||||
sessInfo.LastActivity.Format("15:04:05"),
|
||||
style.Dim.Render(ago))
|
||||
}
|
||||
} else {
|
||||
fmt.Printf(" Status: %s\n", style.Dim.Render("not running"))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// formatActivityTime returns a human-readable relative time string.
|
||||
func formatActivityTime(t time.Time) string {
|
||||
d := time.Since(t)
|
||||
switch {
|
||||
case d < time.Minute:
|
||||
return fmt.Sprintf("%d seconds ago", int(d.Seconds()))
|
||||
case d < time.Hour:
|
||||
return fmt.Sprintf("%d minutes ago", int(d.Minutes()))
|
||||
case d < 24*time.Hour:
|
||||
return fmt.Sprintf("%d hours ago", int(d.Hours()))
|
||||
default:
|
||||
return fmt.Sprintf("%d days ago", int(d.Hours()/24))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,6 +219,24 @@ func ParsePriority(s string) Priority {
|
||||
}
|
||||
}
|
||||
|
||||
// PriorityFromInt converts a beads-style integer priority to a Priority.
|
||||
// Accepts: 0=urgent, 1=high, 2=normal, 3=low, 4=backlog (treated as low).
|
||||
// Invalid values default to PriorityNormal.
|
||||
func PriorityFromInt(p int) Priority {
|
||||
switch p {
|
||||
case 0:
|
||||
return PriorityUrgent
|
||||
case 1:
|
||||
return PriorityHigh
|
||||
case 2:
|
||||
return PriorityNormal
|
||||
case 3, 4:
|
||||
return PriorityLow
|
||||
default:
|
||||
return PriorityNormal
|
||||
}
|
||||
}
|
||||
|
||||
// ParseMessageType parses a message type string, returning TypeNotification for invalid values.
|
||||
func ParseMessageType(s string) MessageType {
|
||||
switch MessageType(s) {
|
||||
|
||||
@@ -69,6 +69,9 @@ type Info struct {
|
||||
|
||||
// Windows is the number of tmux windows.
|
||||
Windows int `json:"windows,omitempty"`
|
||||
|
||||
// LastActivity is when the session last had activity.
|
||||
LastActivity time.Time `json:"last_activity,omitempty"`
|
||||
}
|
||||
|
||||
// sessionName generates the tmux session name for a polecat.
|
||||
@@ -254,6 +257,14 @@ func (m *Manager) Status(polecat string) (*Info, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// Parse activity time (unix timestamp from tmux)
|
||||
if tmuxInfo.Activity != "" {
|
||||
var activityUnix int64
|
||||
if _, err := fmt.Sscanf(tmuxInfo.Activity, "%d", &activityUnix); err == nil && activityUnix > 0 {
|
||||
info.LastActivity = time.Unix(activityUnix, 0)
|
||||
}
|
||||
}
|
||||
|
||||
return info, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -218,10 +218,12 @@ func (t *Tmux) RenameSession(oldName, newName string) error {
|
||||
|
||||
// SessionInfo contains information about a tmux session.
|
||||
type SessionInfo struct {
|
||||
Name string
|
||||
Windows int
|
||||
Created string
|
||||
Attached bool
|
||||
Name string
|
||||
Windows int
|
||||
Created string
|
||||
Attached bool
|
||||
Activity string // Last activity time
|
||||
LastAttached string // Last time the session was attached
|
||||
}
|
||||
|
||||
// DisplayMessage shows a message in the tmux status line.
|
||||
@@ -316,7 +318,7 @@ func (t *Tmux) WaitForShellReady(session string, timeout time.Duration) error {
|
||||
|
||||
// GetSessionInfo returns detailed information about a session.
|
||||
func (t *Tmux) GetSessionInfo(name string) (*SessionInfo, error) {
|
||||
format := "#{session_name}|#{session_windows}|#{session_created_string}|#{session_attached}"
|
||||
format := "#{session_name}|#{session_windows}|#{session_created_string}|#{session_attached}|#{session_activity}|#{session_last_attached}"
|
||||
out, err := t.run("list-sessions", "-F", format, "-f", fmt.Sprintf("#{==:#{session_name},%s}", name))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -326,19 +328,29 @@ func (t *Tmux) GetSessionInfo(name string) (*SessionInfo, error) {
|
||||
}
|
||||
|
||||
parts := strings.Split(out, "|")
|
||||
if len(parts) != 4 {
|
||||
if len(parts) < 4 {
|
||||
return nil, fmt.Errorf("unexpected session info format: %s", out)
|
||||
}
|
||||
|
||||
windows := 0
|
||||
_, _ = fmt.Sscanf(parts[1], "%d", &windows)
|
||||
|
||||
return &SessionInfo{
|
||||
info := &SessionInfo{
|
||||
Name: parts[0],
|
||||
Windows: windows,
|
||||
Created: parts[2],
|
||||
Attached: parts[3] == "1",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Activity and last attached are optional (may not be present in older tmux)
|
||||
if len(parts) > 4 {
|
||||
info.Activity = parts[4]
|
||||
}
|
||||
if len(parts) > 5 {
|
||||
info.LastAttached = parts[5]
|
||||
}
|
||||
|
||||
return info, nil
|
||||
}
|
||||
|
||||
// ApplyTheme sets the status bar style for a session.
|
||||
|
||||
Reference in New Issue
Block a user