feat: Show hook in tmux status bar, fall back to mail if empty
All role status lines now check for hooked work first: - If hook has work: shows hook emoji with bead ID and title - If hook empty: falls back to mail preview Also fixed workspace detection for status-line command by using the pane's working directory instead of relying on cwd. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
committed by
Steve Yegge
parent
40f3a8dfd2
commit
c4d7f3ffeb
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/steveyegge/gastown/internal/beads"
|
||||
"github.com/steveyegge/gastown/internal/mail"
|
||||
"github.com/steveyegge/gastown/internal/tmux"
|
||||
"github.com/steveyegge/gastown/internal/workspace"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -67,7 +68,7 @@ func runStatusLine(cmd *cobra.Command, args []string) error {
|
||||
|
||||
// Refinery status line
|
||||
if role == "refinery" || strings.HasSuffix(statusLineSession, "-refinery") {
|
||||
return runRefineryStatusLine(rigName)
|
||||
return runRefineryStatusLine(t, rigName)
|
||||
}
|
||||
|
||||
// Crew/Polecat status line
|
||||
@@ -86,29 +87,52 @@ func runWorkerStatusLine(t *tmux.Tmux, session, rigName, polecat, crew, issue st
|
||||
identity = fmt.Sprintf("%s/crew/%s", rigName, crew)
|
||||
}
|
||||
|
||||
// Get pane's working directory to find workspace
|
||||
var townRoot string
|
||||
if session != "" {
|
||||
paneDir, err := t.GetPaneWorkDir(session)
|
||||
if err == nil && paneDir != "" {
|
||||
townRoot, _ = workspace.Find(paneDir)
|
||||
}
|
||||
}
|
||||
|
||||
// Build status parts
|
||||
var parts []string
|
||||
|
||||
// Try to get current work from beads if no issue env var
|
||||
// Priority 1: Check for hooked work (use rig beads)
|
||||
hookedWork := ""
|
||||
if identity != "" && rigName != "" && townRoot != "" {
|
||||
rigBeadsDir := filepath.Join(townRoot, rigName, "mayor", "rig")
|
||||
hookedWork = getHookedWork(identity, 40, rigBeadsDir)
|
||||
}
|
||||
|
||||
// Priority 2: Fall back to GT_ISSUE env var or in_progress beads
|
||||
currentWork := issue
|
||||
if currentWork == "" && session != "" {
|
||||
if currentWork == "" && hookedWork == "" && session != "" {
|
||||
currentWork = getCurrentWork(t, session, 40)
|
||||
}
|
||||
|
||||
// Add icon and current work
|
||||
if icon != "" {
|
||||
if currentWork != "" {
|
||||
parts = append(parts, fmt.Sprintf("%s %s", icon, currentWork))
|
||||
// Show hooked work (takes precedence)
|
||||
if hookedWork != "" {
|
||||
if icon != "" {
|
||||
parts = append(parts, fmt.Sprintf("%s 🪝 %s", icon, hookedWork))
|
||||
} else {
|
||||
parts = append(parts, icon)
|
||||
parts = append(parts, fmt.Sprintf("🪝 %s", hookedWork))
|
||||
}
|
||||
} else if currentWork != "" {
|
||||
parts = append(parts, currentWork)
|
||||
// Fall back to current work (in_progress)
|
||||
if icon != "" {
|
||||
parts = append(parts, fmt.Sprintf("%s %s", icon, currentWork))
|
||||
} else {
|
||||
parts = append(parts, currentWork)
|
||||
}
|
||||
} else if icon != "" {
|
||||
parts = append(parts, icon)
|
||||
}
|
||||
|
||||
// Mail preview
|
||||
if identity != "" {
|
||||
unread, subject := getMailPreview(identity, 45)
|
||||
// Mail preview - only show if hook is empty
|
||||
if hookedWork == "" && identity != "" && townRoot != "" {
|
||||
unread, subject := getMailPreviewWithRoot(identity, 45, townRoot)
|
||||
if unread > 0 {
|
||||
if subject != "" {
|
||||
parts = append(parts, fmt.Sprintf("\U0001F4EC %s", subject))
|
||||
@@ -133,6 +157,13 @@ func runMayorStatusLine(t *tmux.Tmux) error {
|
||||
return nil // Silent fail
|
||||
}
|
||||
|
||||
// Get town root from mayor pane's working directory
|
||||
var townRoot string
|
||||
paneDir, err := t.GetPaneWorkDir("gt-mayor")
|
||||
if err == nil && paneDir != "" {
|
||||
townRoot, _ = workspace.Find(paneDir)
|
||||
}
|
||||
|
||||
// Count polecats and rigs
|
||||
// Polecats: only actual polecats (not witnesses, refineries, deacon, crew)
|
||||
// Rigs: any rig with active sessions (witness, refinery, crew, or polecat)
|
||||
@@ -154,18 +185,27 @@ func runMayorStatusLine(t *tmux.Tmux) error {
|
||||
}
|
||||
rigCount := len(rigs)
|
||||
|
||||
// Get mayor mail with preview
|
||||
unread, subject := getMailPreview("mayor/", 45)
|
||||
|
||||
// Build status
|
||||
var parts []string
|
||||
parts = append(parts, fmt.Sprintf("%d 😺", polecatCount))
|
||||
parts = append(parts, fmt.Sprintf("%d rigs", rigCount))
|
||||
if unread > 0 {
|
||||
if subject != "" {
|
||||
parts = append(parts, fmt.Sprintf("\U0001F4EC %s", subject))
|
||||
} else {
|
||||
parts = append(parts, fmt.Sprintf("\U0001F4EC %d", unread))
|
||||
|
||||
// Priority 1: Check for hooked work (town beads for mayor)
|
||||
hookedWork := ""
|
||||
if townRoot != "" {
|
||||
hookedWork = getHookedWork("mayor", 40, townRoot)
|
||||
}
|
||||
if hookedWork != "" {
|
||||
parts = append(parts, fmt.Sprintf("🪝 %s", hookedWork))
|
||||
} else if townRoot != "" {
|
||||
// Priority 2: Fall back to mail preview
|
||||
unread, subject := getMailPreviewWithRoot("mayor/", 45, townRoot)
|
||||
if unread > 0 {
|
||||
if subject != "" {
|
||||
parts = append(parts, fmt.Sprintf("\U0001F4EC %s", subject))
|
||||
} else {
|
||||
parts = append(parts, fmt.Sprintf("\U0001F4EC %d", unread))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,7 +214,7 @@ func runMayorStatusLine(t *tmux.Tmux) error {
|
||||
}
|
||||
|
||||
// runDeaconStatusLine outputs status for the deacon session.
|
||||
// Shows: active rigs, polecat count, mail preview
|
||||
// Shows: active rigs, polecat count, hook or mail preview
|
||||
func runDeaconStatusLine(t *tmux.Tmux) error {
|
||||
// Count active rigs and polecats
|
||||
sessions, err := t.ListSessions()
|
||||
@@ -182,6 +222,13 @@ func runDeaconStatusLine(t *tmux.Tmux) error {
|
||||
return nil // Silent fail
|
||||
}
|
||||
|
||||
// Get town root from deacon pane's working directory
|
||||
var townRoot string
|
||||
paneDir, err := t.GetPaneWorkDir("gt-deacon")
|
||||
if err == nil && paneDir != "" {
|
||||
townRoot, _ = workspace.Find(paneDir)
|
||||
}
|
||||
|
||||
rigs := make(map[string]bool)
|
||||
polecatCount := 0
|
||||
for _, s := range sessions {
|
||||
@@ -198,18 +245,27 @@ func runDeaconStatusLine(t *tmux.Tmux) error {
|
||||
}
|
||||
rigCount := len(rigs)
|
||||
|
||||
// Get deacon mail with preview
|
||||
unread, subject := getMailPreview("deacon/", 40)
|
||||
|
||||
// Build status
|
||||
var parts []string
|
||||
parts = append(parts, fmt.Sprintf("%d rigs", rigCount))
|
||||
parts = append(parts, fmt.Sprintf("%d 😺", polecatCount))
|
||||
if unread > 0 {
|
||||
if subject != "" {
|
||||
parts = append(parts, fmt.Sprintf("\U0001F4EC %s", subject))
|
||||
} else {
|
||||
parts = append(parts, fmt.Sprintf("\U0001F4EC %d", unread))
|
||||
|
||||
// Priority 1: Check for hooked work (town beads for deacon)
|
||||
hookedWork := ""
|
||||
if townRoot != "" {
|
||||
hookedWork = getHookedWork("deacon", 35, townRoot)
|
||||
}
|
||||
if hookedWork != "" {
|
||||
parts = append(parts, fmt.Sprintf("🪝 %s", hookedWork))
|
||||
} else if townRoot != "" {
|
||||
// Priority 2: Fall back to mail preview
|
||||
unread, subject := getMailPreviewWithRoot("deacon/", 40, townRoot)
|
||||
if unread > 0 {
|
||||
if subject != "" {
|
||||
parts = append(parts, fmt.Sprintf("\U0001F4EC %s", subject))
|
||||
} else {
|
||||
parts = append(parts, fmt.Sprintf("\U0001F4EC %d", unread))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -218,7 +274,7 @@ func runDeaconStatusLine(t *tmux.Tmux) error {
|
||||
}
|
||||
|
||||
// runWitnessStatusLine outputs status for a witness session.
|
||||
// Shows: polecat count, crew count, mail preview
|
||||
// Shows: polecat count, crew count, hook or mail preview
|
||||
func runWitnessStatusLine(t *tmux.Tmux, rigName string) error {
|
||||
if rigName == "" {
|
||||
// Try to extract from session name: gt-<rig>-witness
|
||||
@@ -227,6 +283,14 @@ func runWitnessStatusLine(t *tmux.Tmux, rigName string) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Get town root from witness pane's working directory
|
||||
var townRoot string
|
||||
sessionName := fmt.Sprintf("gt-%s-witness", rigName)
|
||||
paneDir, err := t.GetPaneWorkDir(sessionName)
|
||||
if err == nil && paneDir != "" {
|
||||
townRoot, _ = workspace.Find(paneDir)
|
||||
}
|
||||
|
||||
// Count polecats and crew in this rig
|
||||
sessions, err := t.ListSessions()
|
||||
if err != nil {
|
||||
@@ -249,9 +313,7 @@ func runWitnessStatusLine(t *tmux.Tmux, rigName string) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Get witness mail with preview
|
||||
identity := fmt.Sprintf("%s/witness", rigName)
|
||||
unread, subject := getMailPreview(identity, 35)
|
||||
|
||||
// Build status
|
||||
var parts []string
|
||||
@@ -259,11 +321,24 @@ func runWitnessStatusLine(t *tmux.Tmux, rigName string) error {
|
||||
if crewCount > 0 {
|
||||
parts = append(parts, fmt.Sprintf("%d crew", crewCount))
|
||||
}
|
||||
if unread > 0 {
|
||||
if subject != "" {
|
||||
parts = append(parts, fmt.Sprintf("\U0001F4EC %s", subject))
|
||||
} else {
|
||||
parts = append(parts, fmt.Sprintf("\U0001F4EC %d", unread))
|
||||
|
||||
// Priority 1: Check for hooked work (rig beads for witness)
|
||||
hookedWork := ""
|
||||
if townRoot != "" && rigName != "" {
|
||||
rigBeadsDir := filepath.Join(townRoot, rigName, "mayor", "rig")
|
||||
hookedWork = getHookedWork(identity, 30, rigBeadsDir)
|
||||
}
|
||||
if hookedWork != "" {
|
||||
parts = append(parts, fmt.Sprintf("🪝 %s", hookedWork))
|
||||
} else if townRoot != "" {
|
||||
// Priority 2: Fall back to mail preview
|
||||
unread, subject := getMailPreviewWithRoot(identity, 35, townRoot)
|
||||
if unread > 0 {
|
||||
if subject != "" {
|
||||
parts = append(parts, fmt.Sprintf("\U0001F4EC %s", subject))
|
||||
} else {
|
||||
parts = append(parts, fmt.Sprintf("\U0001F4EC %d", unread))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -272,8 +347,8 @@ func runWitnessStatusLine(t *tmux.Tmux, rigName string) error {
|
||||
}
|
||||
|
||||
// runRefineryStatusLine outputs status for a refinery session.
|
||||
// Shows: MQ length, current item, mail preview
|
||||
func runRefineryStatusLine(rigName string) error {
|
||||
// Shows: MQ length, current item, hook or mail preview
|
||||
func runRefineryStatusLine(t *tmux.Tmux, rigName string) error {
|
||||
if rigName == "" {
|
||||
// Try to extract from session name: gt-<rig>-refinery
|
||||
if strings.HasPrefix(statusLineSession, "gt-") && strings.HasSuffix(statusLineSession, "-refinery") {
|
||||
@@ -287,6 +362,14 @@ func runRefineryStatusLine(rigName string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get town root from refinery pane's working directory
|
||||
var townRoot string
|
||||
sessionName := fmt.Sprintf("gt-%s-refinery", rigName)
|
||||
paneDir, err := t.GetPaneWorkDir(sessionName)
|
||||
if err == nil && paneDir != "" {
|
||||
townRoot, _ = workspace.Find(paneDir)
|
||||
}
|
||||
|
||||
// Get refinery manager using shared helper
|
||||
mgr, _, _, err := getRefineryManager(rigName)
|
||||
if err != nil {
|
||||
@@ -315,9 +398,7 @@ func runRefineryStatusLine(rigName string) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Get refinery mail with preview
|
||||
identity := fmt.Sprintf("%s/refinery", rigName)
|
||||
unread, subject := getMailPreview(identity, 30)
|
||||
|
||||
// Build status
|
||||
var parts []string
|
||||
@@ -332,11 +413,23 @@ func runRefineryStatusLine(rigName string) error {
|
||||
parts = append(parts, "idle")
|
||||
}
|
||||
|
||||
if unread > 0 {
|
||||
if subject != "" {
|
||||
parts = append(parts, fmt.Sprintf("\U0001F4EC %s", subject))
|
||||
} else {
|
||||
parts = append(parts, fmt.Sprintf("\U0001F4EC %d", unread))
|
||||
// Priority 1: Check for hooked work (rig beads for refinery)
|
||||
hookedWork := ""
|
||||
if townRoot != "" && rigName != "" {
|
||||
rigBeadsDir := filepath.Join(townRoot, rigName, "mayor", "rig")
|
||||
hookedWork = getHookedWork(identity, 25, rigBeadsDir)
|
||||
}
|
||||
if hookedWork != "" {
|
||||
parts = append(parts, fmt.Sprintf("🪝 %s", hookedWork))
|
||||
} else if townRoot != "" {
|
||||
// Priority 2: Fall back to mail preview
|
||||
unread, subject := getMailPreviewWithRoot(identity, 30, townRoot)
|
||||
if unread > 0 {
|
||||
if subject != "" {
|
||||
parts = append(parts, fmt.Sprintf("\U0001F4EC %s", subject))
|
||||
} else {
|
||||
parts = append(parts, fmt.Sprintf("\U0001F4EC %d", unread))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -390,6 +483,61 @@ func getMailPreview(identity string, maxLen int) (int, string) {
|
||||
return len(messages), subject
|
||||
}
|
||||
|
||||
// getMailPreviewWithRoot is like getMailPreview but uses an explicit town root.
|
||||
func getMailPreviewWithRoot(identity string, maxLen int, townRoot string) (int, string) {
|
||||
// Use NewMailboxFromAddress to normalize identity (e.g., gastown/crew/gus -> gastown/gus)
|
||||
mailbox := mail.NewMailboxFromAddress(identity, townRoot)
|
||||
|
||||
// Get unread messages
|
||||
messages, err := mailbox.ListUnread()
|
||||
if err != nil || len(messages) == 0 {
|
||||
return 0, ""
|
||||
}
|
||||
|
||||
// Get first message subject, truncated
|
||||
subject := messages[0].Subject
|
||||
if len(subject) > maxLen {
|
||||
subject = subject[:maxLen-1] + "…"
|
||||
}
|
||||
|
||||
return len(messages), subject
|
||||
}
|
||||
|
||||
// getHookedWork returns a truncated title of the hooked bead for an agent.
|
||||
// Returns empty string if nothing is hooked.
|
||||
// beadsDir should be the directory containing .beads (for rig-level) or
|
||||
// empty to use the town root (for town-level roles).
|
||||
func getHookedWork(identity string, maxLen int, beadsDir string) string {
|
||||
// If no beadsDir specified, use town root
|
||||
if beadsDir == "" {
|
||||
var err error
|
||||
beadsDir, err = findMailWorkDir()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
b := beads.New(beadsDir)
|
||||
|
||||
// Query for hooked beads assigned to this agent
|
||||
hookedBeads, err := b.List(beads.ListOptions{
|
||||
Status: beads.StatusHooked,
|
||||
Assignee: identity,
|
||||
Priority: -1,
|
||||
})
|
||||
if err != nil || len(hookedBeads) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Return first hooked bead's ID and title, truncated
|
||||
bead := hookedBeads[0]
|
||||
display := fmt.Sprintf("%s: %s", bead.ID, bead.Title)
|
||||
if len(display) > maxLen {
|
||||
display = display[:maxLen-1] + "…"
|
||||
}
|
||||
return display
|
||||
}
|
||||
|
||||
// getCurrentWork returns a truncated title of the first in_progress issue.
|
||||
// Uses the pane's working directory to find the beads.
|
||||
func getCurrentWork(t *tmux.Tmux, session string, maxLen int) string {
|
||||
|
||||
Reference in New Issue
Block a user