Files
OpenOPC/opc/plugins/cli_board/widgets/session_sidebar.py
T
2026-07-01 17:56:31 +08:00

182 lines
6.8 KiB
Python

"""Session tree widget for the CLI board.
Displays tasks in a tree structure where parent tasks are expandable nodes
and child work items appear as leaves beneath them.
"""
from __future__ import annotations
from rich.console import Group, RenderableType
from rich.panel import Panel
from rich.text import Text
from textual.widgets import Static
from ..state.models import BoardTaskView, SessionSummaryView
from ..state.store import BoardStateStore
from .render_utils import badge, humanize_age, priority_style, status_style, truncate_text
_STATUS_SYMBOL = {
"done": ("\u2713", "bold #22c55e"),
"running": ("\u25cf", "bold #38bdf8"),
"idle": ("\u25cf", "bold #38bdf8"),
"pending": ("\u25cb", "dim"),
"failed": ("\u2717", "bold #ef4444"),
"cancelled": ("\u2717", "bold #9333ea"),
"blocked": ("\u25a0", "bold #f59e0b"),
"awaiting_peer": ("\u25a0", "bold #f59e0b"),
"awaiting_review": ("\u25a0", "bold #f59e0b"),
}
class SessionSidebarWidget(Static):
"""Render a session tree with parent-child relationships."""
def __init__(self, state: BoardStateStore) -> None:
super().__init__(id="session-sidebar")
self.state = state
def render(self) -> RenderableType:
company_mode = self.state.snapshot.mode == "company"
title = "Runtime Sessions" if company_mode else "Sessions"
if self.state.pane_focus == "session-rail":
title += " [Focused]"
tree = self._build_tree()
if not tree:
return Panel(
Text("No runtime sessions available." if company_mode else "No sessions available.", style="dim"),
title=title,
border_style="cyan" if self.state.pane_focus == "session-rail" else "white",
)
return Panel(
Group(*tree),
title=title,
border_style="cyan" if self.state.pane_focus == "session-rail" else "white",
)
def _build_tree(self) -> list[RenderableType]:
"""Build a tree from tasks, grouping children under parents."""
all_tasks = self.state.filtered_tasks()
if not all_tasks:
return []
# Separate parent (visible/top-level) tasks from child tasks
# Child tasks have origin_task_id pointing to their parent
parent_tasks: list[BoardTaskView] = []
children_by_parent: dict[str, list[BoardTaskView]] = {}
# First pass: identify parents and children from the visible task list
# Also look at linked executions from the snapshot data
visible_ids = {t.task_id for t in all_tasks}
for task in all_tasks:
if task.origin_task_id and task.origin_task_id in visible_ids:
children_by_parent.setdefault(task.origin_task_id, []).append(task)
else:
parent_tasks.append(task)
# Also pull in hidden linked tasks from snapshot metadata
for task in self.state.all_tasks():
if task.task_id in visible_ids:
continue
if task.origin_task_id and task.origin_task_id in visible_ids:
children_by_parent.setdefault(task.origin_task_id, []).append(task)
# Sort parents: live first, then queue, then archive
parent_tasks.sort(key=lambda t: (
0 if self._is_live(t) else 1 if t.column_id != "done" else 2,
-float(t.updated_at),
))
lines: list[RenderableType] = []
for parent in parent_tasks:
children = children_by_parent.get(parent.task_id, [])
children.sort(key=lambda t: (float(t.created_at), t.title))
lines.append(self._render_parent(parent, children))
return lines
def _render_parent(self, task: BoardTaskView, children: list[BoardTaskView]) -> Text:
selected = task.task_id == self.state.selected_task_id
runtime = self.state.runtime_for(task.task_id)
has_children = bool(children)
sym, sym_style = _STATUS_SYMBOL.get(task.status, ("\u25cb", "dim"))
text = Text()
# Expand/collapse indicator
if has_children:
text.append("\u25bc ", style="bold white") # ▼
else:
text.append(" ", style="")
# Selection marker
if selected:
text.append("\u25c6 ", style="bold cyan") # ◆
else:
text.append(" ", style="")
# Status + title
text.append(f"{sym} ", style=sym_style)
title_style = "bold white" if selected else "white"
text.append(truncate_text(task.title, 22), style=title_style)
text.append(f" {humanize_age(task.updated_at)}", style="dim")
# Runtime info
if runtime and runtime.current_tool:
text.append(f" \u2699{runtime.current_tool}", style="dim")
# Badges on next line if relevant
if task.pending_checkpoint or task.priority:
text.append("\n ")
if task.priority:
text += badge(task.priority.upper(), priority_style(task.priority))
text.append(" ")
if task.pending_checkpoint:
text += badge("REVIEW", status_style("warn"))
# Children
for i, child in enumerate(children[:10]):
is_last = i == len(children) - 1 or i == 9
text.append("\n")
text.append(self._render_child(child, is_last=is_last))
if len(children) > 10:
text.append(f"\n \u2026 +{len(children) - 10} more", style="dim")
text.append("\n")
return text
def _render_child(self, task: BoardTaskView, *, is_last: bool) -> Text:
selected = task.task_id == self.state.selected_task_id
runtime = self.state.runtime_for(task.task_id)
sym, sym_style = _STATUS_SYMBOL.get(task.status, ("\u25cb", "dim"))
connector = " \u2514\u2500 " if is_last else " \u251c\u2500 " # └─ or ├─
text = Text()
text.append(connector, style="dim")
if selected:
text.append(f"{sym} ", style=sym_style)
text.append(truncate_text(task.title, 18), style="bold cyan")
else:
text.append(f"{sym} ", style=sym_style)
text.append(truncate_text(task.title, 18), style="white")
# Assignee
if task.assigned_to:
text.append(f" {truncate_text(task.assigned_to, 10)}", style="dim")
# Elapsed
text.append(f" {humanize_age(task.updated_at)}", style="dim")
# Current tool
if runtime and runtime.current_tool:
text.append(f" \u2699{truncate_text(runtime.current_tool, 12)}", style="dim")
return text
@staticmethod
def _is_live(task: BoardTaskView) -> bool:
return task.status in {"running", "idle", "blocked", "awaiting_peer", "awaiting_review"} or task.pending_checkpoint is not None