Files
patterm/internal/app/tree.go

236 lines
6.0 KiB
Go

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) {
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) == 0 {
return ""
}
if len(flat) == 1 {
if flat[0].ID == focusID {
return ""
}
return flat[0].ID
}
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
}