package app // visibleAgentTree returns the running entries under the active agent // tab (root agent + its sub-agents). With the new Processes pane, // command processes live in their own section and never show up here — // the agent tree is for KindAgent (and KindTerminal sub-entries) only. func visibleAgentTree(children []*Child, activeAgentID string) []*Child { if activeAgentID == "" { return nil } out := make([]*Child, 0, len(children)) for _, c := range children { if c.Status() != StatusRunning { continue } if c.Kind == KindCommand && c.ParentID == "" { continue } if c.ID == activeAgentID || c.ParentID == activeAgentID { out = append(out, c) } } return out } // visibleSessionTree is retained for the test suite and any pre-Processes // callers — it returns the active agent's tree given the focused id, // resolving the active agent root from focus the same way the previous // implementation did. func visibleSessionTree(children []*Child, focusID string) []*Child { rootID := activeRootID(children, focusID) if rootID == "" { return nil } out := make([]*Child, 0, len(children)) for _, c := range children { if c.Status() != StatusRunning { continue } if c.ID == rootID || c.ParentID == rootID { out = append(out, c) } } return out } // activeRootID resolves the agent root the user is "inside" right now. // If focus is on a sub-agent, it walks up. If focus is on a top-level // process (KindCommand), it falls through to the first running agent // root so the agent tree section keeps showing something coherent. func activeRootID(children []*Child, focusID string) string { if focusID != "" { for _, c := range children { if c.ID != focusID { continue } if c.Kind == KindCommand && c.ParentID == "" { break } if c.ParentID == "" { return c.ID } if parent := findChildInSnapshot(children, c.ParentID); parent != nil { return parent.ID } return "" } } return firstRunningAgentID(children) } func firstRunningAgentID(children []*Child) string { for _, c := range children { if c.Kind != KindAgent { continue } if c.ParentID == "" && c.Status() == StatusRunning { return c.ID } } return "" } // processList returns every top-level command/terminal entry in spawn // order, regardless of running state. The Processes sidebar section // keeps showing exited entries so the user can see what just died (and // because Session retains KindCommand entries for restart). func processList(children []*Child) []*Child { out := make([]*Child, 0, len(children)) for _, c := range children { if c.ParentID != "" { continue } if c.Kind == KindCommand || c.Kind == KindTerminal { out = append(out, c) } } return out } func findChildInSnapshot(children []*Child, id string) *Child { for _, c := range children { if c.ID == id { return c } } return nil } func firstRunningTopLevel(children []*Child) *Child { for _, c := range children { if c.ParentID == "" && c.Status() == StatusRunning { return c } } return nil } // runningTopLevels lists every running top-level agent session in the // order they appear in the snapshot. Tabs only show agents — command // processes live in the Processes sidebar section — so Ctrl+A/D // navigation cycles through agent tabs exclusively. func runningTopLevels(children []*Child) []*Child { out := make([]*Child, 0, len(children)) for _, c := range children { if c.Kind != KindAgent { continue } if c.ParentID == "" && c.Status() == StatusRunning { out = append(out, c) } } return out } // nextTabID returns the id of the top-level session `step` positions // away from the current focus in the runningTopLevels list, wrapping // at both ends. Returns "" when there's nothing to switch to. func nextTabID(children []*Child, focusID string, step int) string { roots := runningTopLevels(children) if len(roots) == 0 { return "" } rootID := activeRootID(children, focusID) idx := -1 for i, r := range roots { if r.ID == rootID { idx = i break } } if idx < 0 { idx = 0 } idx = (idx + step) % len(roots) if idx < 0 { idx += len(roots) } if roots[idx].ID == focusID { return "" } return roots[idx].ID } // currentTabFlat returns the focused tab's processes (root first, then // its running children) in display order. Used to step focus with // Ctrl+W/S. func currentTabFlat(children []*Child, focusID string) []*Child { rootID := activeRootID(children, focusID) if rootID == "" { return nil } out := make([]*Child, 0, 4) for _, c := range children { if c.ID == rootID && c.Status() == StatusRunning { out = append(out, c) break } } for _, c := range children { if c.ParentID == rootID && c.Status() == StatusRunning { out = append(out, c) } } return out } // sidebarNavList combines the Processes section and the active Agent // Tree into one flat list — top-to-bottom matching what the user sees // in the sidebar. Ctrl+W/S walks this list so the user can step out of // the agent tree, into the Processes section, and back. func sidebarNavList(children []*Child, activeAgentID string) []*Child { out := make([]*Child, 0, 8) for _, c := range processList(children) { if c.Status() != StatusRunning { continue } out = append(out, c) } for _, c := range visibleAgentTree(children, activeAgentID) { out = append(out, c) } return out } // nextChildID returns the id `step` positions away from the current // focus in the combined Processes + active-agent-tree navigation list, // wrapping at both ends. Empty when there's nothing else to land on. func nextChildID(children []*Child, focusID, activeAgentID string, step int) string { flat := sidebarNavList(children, activeAgentID) if len(flat) < 2 { return "" } idx := -1 for i, c := range flat { if c.ID == focusID { idx = i break } } if idx < 0 { idx = 0 } idx = (idx + step) % len(flat) if idx < 0 { idx += len(flat) } if flat[idx].ID == focusID { return "" } return flat[idx].ID }