package modes import ( "fmt" "strings" "time" "github.com/bnema/zut/packages/agent/subagents" "github.com/bnema/zut/packages/tui" "github.com/mattn/go-runewidth" ) var residentProviderWaitingSpinner = tui.NewStringSpinner( []string{"Zzzz", "zZzz", "zzZz", "zzzZ"}, time.Second, ) // renderResidentSubagentActivityLines renders the bounded, non-sensitive // background work indicator shown directly below the main editor. A queued // child is waiting for a resident scheduler slot; only running children use // the animated glyph. func renderResidentSubagentActivityLines(theme tui.Theme, spinnerGlyph string, snapshots []subagents.ResidentSnapshot, width int, now time.Time) []string { lines := make([]string, 0, len(snapshots)*2) for _, snapshot := range snapshots { // Foreign journals remain discoverable through /subagents, but their // activity belongs to another zut host and must not occupy this // session's always-visible editor-adjacent indicator. if snapshot.OwnedElsewhere || (snapshot.State != subagents.ResidentQueued && snapshot.State != subagents.ResidentRunning) { continue } name := sanitizeSessionTreeText(snapshot.Profile) if name == "" { name = sanitizeSessionTreeText(snapshot.ID) } if name == "" { name = "subagent" } glyph := "…" color := theme.Muted activity := "queued" if snapshot.State == subagents.ResidentRunning { glyph = strings.TrimSpace(spinnerGlyph) if glyph == "" { glyph = "." } color = theme.Spinner if snapshot.WaitingForModel { activity = residentProviderWaitingSpinner.FrameAt(activityTime(snapshot), now) } else { activity = "running" } } plain := fmt.Sprintf("%s %s · %s · activity %s ago · %s", glyph, name, activity, formatResidentSubagentActivityAge(activityTime(snapshot), now), formatResidentSubagentActivityAge(turnStartTime(snapshot), now)) plain = truncateResidentSubagentIndicator(plain, width-2) if plain == "" { continue } lines = append(lines, " "+theme.FGColor(color, plain)) if metadata := residentUsageMetadata(snapshot); metadata != "" { metadata = truncateResidentSubagentIndicator(metadata, width-4) if metadata != "" { lines = append(lines, " "+theme.FGColor(theme.Muted, metadata)) } } } return lines } func residentUsageMetadata(snapshot subagents.ResidentSnapshot) string { parts := tui.UsageStatsParts(tui.UsageStatsParams{Usage: snapshot.Usage, Subscription: snapshot.Subscription}) if context := tui.ContextUsageText(snapshot.ContextUsed, snapshot.ContextMax); context != "" { parts = append(parts, context) } return strings.Join(parts, " ") } func turnStartTime(snapshot subagents.ResidentSnapshot) time.Time { if !snapshot.TurnStartedAt.IsZero() { return snapshot.TurnStartedAt } return snapshot.UpdatedAt } func activityTime(snapshot subagents.ResidentSnapshot) time.Time { if !snapshot.ActivityUpdatedAt.IsZero() { return snapshot.ActivityUpdatedAt } return snapshot.UpdatedAt } func formatResidentSubagentActivityAge(started, now time.Time) string { if started.IsZero() { return "-" } if now.IsZero() { now = time.Now() } elapsed := now.Sub(started) if elapsed < 0 { elapsed = 0 } seconds := int(elapsed.Seconds()) switch { case seconds < 60: return fmt.Sprintf("%ds", seconds) case seconds < 60*60: return fmt.Sprintf("%dm%02ds", seconds/60, seconds%60) case seconds < 24*60*60: return fmt.Sprintf("%dh%02dm", seconds/(60*60), (seconds%(60*60))/60) default: return fmt.Sprintf("%dd%02dh", seconds/(24*60*60), (seconds%(24*60*60))/(60*60)) } } func limitResidentSubagentActivityLines(theme tui.Theme, lines []string, hidden, maxRows, width int) []string { if maxRows <= 0 || (len(lines) == 0 && hidden == 0) { return nil } groups := make([][]string, 0, len(lines)) for _, line := range lines { if strings.HasPrefix(line, " ") && len(groups) > 0 { groups[len(groups)-1] = append(groups[len(groups)-1], line) continue } groups = append(groups, []string{line}) } if hidden == 0 && len(lines) <= maxRows { return lines } if maxRows == 1 { return []string{residentSubagentActivityOverflowLine(theme, len(groups)+hidden, width)} } out := make([]string, 0, maxRows) visibleGroups := 0 for _, group := range groups { if len(out)+len(group) > maxRows-1 { break } out = append(out, group...) visibleGroups++ } return append(out, residentSubagentActivityOverflowLine(theme, hidden+len(groups)-visibleGroups, width)) } func residentSubagentActivityOverflowLine(theme tui.Theme, hidden, width int) string { text := fmt.Sprintf(" … %d more active subagents", hidden) return theme.FGColor(theme.Muted, truncateResidentSubagentIndicator(text, width)) } func truncateResidentSubagentIndicator(s string, width int) string { if width <= 0 { return "" } if runewidth.StringWidth(s) <= width { return s } if width <= 3 { return strings.Repeat(".", width) } return runewidth.Truncate(s, width, "...") }