Files
2026-07-01 17:56:31 +08:00

409 lines
18 KiB
Python

"""Task detail pane with structured checkpoint panels."""
from __future__ import annotations
import math
from typing import Any
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 PendingCheckpointView, TaskDetailView
from ..state.store import BoardStateStore
from .render_utils import adaptive_summary, badge, format_clock, humanize_age, priority_style, status_style, truncate_text
_RISK_STYLES = {
"low": "bold #22c55e",
"medium": "bold #f59e0b",
"high": "bold #ef4444",
"critical": "bold white on #ef4444",
}
_SCOPE_LABELS = {
"task_adjustment": "Task Adjustment",
"org_mutation": "Org Mutation",
}
_CHANGE_ACTION_STYLES = {
"add": ("+ ", "bold #22c55e"),
"remove": ("- ", "bold #ef4444"),
"replace": ("~ ", "bold #f59e0b"),
"update": ("~ ", "bold #f59e0b"),
}
def _progress_bar(ratio: float | None, width: int = 10) -> str:
"""Render a block progress bar: ████████░░"""
try:
val = float(ratio if ratio is not None else 0)
if math.isnan(val) or math.isinf(val):
val = 0.0
clamped = max(0.0, min(1.0, val))
except (TypeError, ValueError):
clamped = 0.0
filled = int(clamped * width)
return "\u2588" * filled + "\u2591" * (width - filled)
class DetailPaneWidget(Static):
"""Render the selected task's metadata and linked executions."""
def __init__(self, state: BoardStateStore) -> None:
super().__init__(id="detail-pane")
self.state = state
self.detail: TaskDetailView | None = None
def set_detail(self, detail: TaskDetailView | None) -> None:
self.detail = detail
self.refresh()
def render(self) -> RenderableType:
focused = self.state.pane_focus == "context" and self.state.context_tab == "detail"
company_mode = self.state.snapshot.mode == "company"
detail_title = "Work Item Detail" if company_mode else "Task Detail"
if self.detail is None:
return Panel(
Text(
"Select a work item to inspect details." if company_mode else "Select a task to inspect details.",
style="dim",
),
title=f"{detail_title} [Focused]" if focused else detail_title,
border_style="cyan" if focused else "white",
)
task = self.detail.task
runtime = self.state.runtime_for(task.task_id)
blocks: list[RenderableType] = [self._render_overview(task, runtime)]
if self.detail.pending_checkpoint:
blocks.append(self._render_checkpoint())
if self.detail.linked_executions:
blocks.append(self._render_linked())
if self.detail.result_content:
blocks.append(self._render_section("Latest Result", self.detail.result_content, 560))
if self.detail.context_preview:
blocks.append(self._render_section("Context Preview", self.detail.context_preview, 560))
return Panel(
Group(*blocks),
title=f"{detail_title} [Focused]" if focused else detail_title,
border_style="cyan" if focused else "white",
)
def _render_overview(self, task: Any, runtime: Any) -> Text:
adaptive = adaptive_summary(task.metadata)
header = Text()
header.append(f"{task.display_id or task.task_id}\n", style="bold #38bdf8")
header.append(f"{task.title}\n", style="bold white")
header += badge(task.status.upper(), status_style(task.status))
if task.priority:
header.append(" ")
header += badge(task.priority.upper(), priority_style(task.priority))
if task.pending_checkpoint:
header.append(" ")
header += badge("REVIEW", status_style("warn"))
if runtime and runtime.status not in {"idle", ""}:
header.append(" ")
header += badge(runtime.status.upper(), status_style(runtime.status))
header.append("\n")
header.append(f"column {task.column_id}", style="dim")
header.append(f" updated {humanize_age(task.updated_at)}", style="dim")
header.append(f" created {format_clock(task.created_at)}", style="dim")
if task.assigned_to:
header.append(f"\nowner {task.assigned_to}", style="dim")
if task.tags:
header.append(f"\ntags {truncate_text(', '.join(task.tags), 70)}", style="dim")
if task.dependencies:
header.append(f"\ndeps {truncate_text(', '.join(task.dependencies), 70)}", style="dim")
if adaptive["state"]:
header.append(f"\nadaptive {adaptive['state']}", style="dim")
if adaptive["gate_owner"]:
header.append(f"\ngate owner {adaptive['gate_owner']}", style="dim")
if adaptive["missing_signals"]:
header.append(
f"\nmissing signals {truncate_text(', '.join(adaptive['missing_signals']), 70)}",
style="dim",
)
if adaptive["confidence_label"]:
header.append(f"\nconfidence {adaptive['confidence_label']}", style="dim")
if adaptive["blocked_reason"]:
header.append(f"\nwaiting {truncate_text(adaptive['blocked_reason'], 120)}", style="dim")
if runtime and runtime.current_tool:
header.append(f"\nactive tool {runtime.current_tool}", style="dim")
if task.description:
header.append(f"\n\n{truncate_text(task.description, 420)}", style="white")
return header
# ----- Checkpoint dispatcher -----
def _render_checkpoint(self) -> RenderableType:
checkpoint = self.detail.pending_checkpoint
assert checkpoint is not None
cp_type = checkpoint.checkpoint_type.strip().lower()
if cp_type == "company_staffing_selection":
return self._render_staffing_checkpoint(checkpoint)
if cp_type == "company_recruitment_confirmation":
return self._render_recruitment_checkpoint(checkpoint)
if cp_type == "company_reorg_pending":
return self._render_reorg_checkpoint(checkpoint)
if cp_type == "human_escalation":
return self._render_escalation_checkpoint(checkpoint)
return self._render_generic_checkpoint(checkpoint)
# ----- Recruitment -----
def _render_staffing_checkpoint(self, checkpoint: PendingCheckpointView) -> RenderableType:
payload = checkpoint.payload
roles = payload.get("staffing_roles", []) or []
pool = payload.get("staffing_pool", {}) or {}
employees = {
str(item.get("employee_id", "") or ""): item
for item in list(pool.get("employees", []) or [])
if str(item.get("employee_id", "") or "")
}
profile = payload.get("company_profile", "")
header = Text()
header.append("MANUAL STAFFING", style="bold #22c55e")
header.append(" ")
header += badge("PENDING", status_style("warn"))
if profile:
header.append(f" {profile}", style="dim italic")
parts: list[RenderableType] = [header]
for index, role in enumerate(roles, start=1):
role_id = str(role.get("role_id", "") or "?")
role_label = str(role.get("role_label", "") or role_id)
selection = role.get("default_selection", {}) or {}
text = Text(f"\n{index}. ", style="bold white")
text.append(role_id, style="bold #38bdf8")
if role_label and role_label != role_id:
text.append(f" {role_label}", style="dim")
if selection.get("kind") == "employee":
employee_id = str(selection.get("employee_id") or selection.get("id") or "")
employee = employees.get(employee_id, {})
name = employee.get("employee_name") or employee_id
text.append(f"\n default: {name}", style="bold white")
text.append(f" ({employee_id})", style="dim")
else:
text.append("\n default: fallback role-only", style="dim")
parts.append(text)
parts.append(Text("\n[a] Approve defaults [r] Auto Recruit [d] Deny", style="dim italic"))
return Panel(Group(*parts), title="Checkpoint: Manual Staffing", border_style="#22c55e")
def _render_recruitment_checkpoint(self, checkpoint: PendingCheckpointView) -> RenderableType:
payload = checkpoint.payload
plan = payload.get("recruitment_plan", {})
proposals = plan.get("proposals", [])
summary_text = plan.get("summary", "") or checkpoint.summary
profile = plan.get("company_profile", "")
header = Text()
header.append("RECRUITMENT", style="bold #fbbf24")
header.append(" ")
header += badge("PENDING", status_style("warn"))
if profile:
header.append(f" {profile}", style="dim italic")
parts: list[RenderableType] = [header]
if summary_text:
parts.append(Text(f"\n{truncate_text(summary_text, 280)}", style="white"))
for i, proposal in enumerate(proposals):
parts.append(self._render_proposal(i + 1, proposal))
parts.append(Text("\n[a] Approve [d] Deny [e] Feedback", style="dim italic"))
return Panel(Group(*parts), title="Checkpoint: Recruitment", border_style="#fbbf24")
def _render_proposal(self, index: int, proposal: dict[str, Any]) -> Text:
role_id = proposal.get("role_id") or "?"
status = proposal.get("status") or ""
status_label = {"proposed_hire": "New Hire", "existing_staff": "Existing"}.get(status, status or "Fallback")
role_labels = proposal.get("role_labels") or []
text = Text(f"\n{index}. ", style="bold white")
text.append(role_id, style="bold #38bdf8")
text.append(" ")
text += badge(status_label, "bold black on #64748b")
if role_labels:
text.append(f"\n roles: {', '.join(str(label) for label in role_labels)}", style="dim")
candidate = proposal.get("candidate")
if candidate and isinstance(candidate, dict):
name = candidate.get("proposed_employee_name") or candidate.get("template_name") or "unnamed"
category = candidate.get("category") or ""
domains = candidate.get("domains") or []
rationale = candidate.get("rationale") or ""
text.append(f"\n {name}", style="bold white")
if category:
text.append(f" [{category}]", style="dim")
if domains:
text.append(f"\n domains: {', '.join(str(d) for d in domains[:6])}", style="#a78bfa")
if rationale:
text.append(f"\n {truncate_text(str(rationale), 120)}", style="dim italic")
existing = proposal.get("existing_employee")
if existing and isinstance(existing, dict):
emp_name = existing.get("employee_name") or "?"
emp_id = existing.get("employee_id") or ""
score_raw = existing.get("experience_score")
score = float(score_raw) if score_raw is not None else 0.0
domains = existing.get("domains") or []
rationale = existing.get("rationale") or ""
text.append(f"\n {emp_name}", style="bold white")
if emp_id:
text.append(f" ({emp_id})", style="dim")
text.append(f"\n score: {_progress_bar(score)} {int(score * 100)}%", style="#22c55e")
if domains:
text.append(f"\n domains: {', '.join(str(d) for d in domains[:6])}", style="#a78bfa")
if rationale:
text.append(f"\n {truncate_text(str(rationale), 120)}", style="dim italic")
top_rationale = proposal.get("rationale") or ""
if top_rationale and not candidate and not existing:
text.append(f"\n {truncate_text(str(top_rationale), 120)}", style="dim italic")
return text
# ----- Reorg -----
def _render_reorg_checkpoint(self, checkpoint: PendingCheckpointView) -> RenderableType:
payload = checkpoint.payload
title = payload.get("title", "") or "Company Reorg"
scope = payload.get("scope", "org_mutation")
risk = payload.get("risk_level", "medium")
summary = payload.get("summary", "") or checkpoint.summary
rationale = payload.get("rationale", "")
role_changes = payload.get("role_changes", [])
impact = payload.get("impact_summary", {})
header = Text()
header.append("REORG", style="bold #fbbf24")
header.append(" ")
scope_label = _SCOPE_LABELS.get(scope, scope)
header += badge(scope_label, "bold black on #64748b")
header.append(" Risk: ", style="dim")
risk_style = _RISK_STYLES.get(risk, "bold #f59e0b")
header.append(f"\u25a0 {risk.upper()}", style=risk_style)
parts: list[RenderableType] = [header]
if summary:
parts.append(Text(f"\n{truncate_text(summary, 280)}", style="white"))
if rationale:
parts.append(Text(f"\n{truncate_text(rationale, 200)}", style="dim italic"))
if role_changes:
changes_text = Text("\nRole Changes:", style="bold #cbd5e1")
for rc in role_changes[:8]:
action = rc.get("action", "?")
prefix, style = _CHANGE_ACTION_STYLES.get(action, ("? ", "bold white"))
changes_text.append(f"\n {prefix}", style=style)
changes_text.append(rc.get("role_id", "?"), style="bold white")
replacement = rc.get("replacement_role_id", "")
if replacement:
changes_text.append(f" \u2192 {replacement}", style="dim")
reason = rc.get("reason", "")
if reason:
changes_text.append(f" {truncate_text(reason, 50)}", style="dim italic")
parts.append(changes_text)
if impact:
impact_text = Text("\nImpact: ", style="dim")
impact_parts = []
if impact.get("affected_roles") is not None:
impact_parts.append(f"{impact['affected_roles']} roles")
if impact.get("affected_tasks") is not None:
impact_parts.append(f"{impact['affected_tasks']} tasks")
if impact.get("migration_count") is not None:
impact_parts.append(f"{impact['migration_count']} migrations")
if impact_parts:
impact_text.append(", ".join(impact_parts), style="white")
parts.append(impact_text)
parts.append(Text("\n[a] Approve [d] Deny [e] Feedback", style="dim italic"))
return Panel(Group(*parts), title=f"Checkpoint: {title}", border_style="#fbbf24")
# ----- Escalation -----
def _render_escalation_checkpoint(self, checkpoint: PendingCheckpointView) -> RenderableType:
payload = checkpoint.payload
prompt = payload.get("prompt", "") or payload.get("summary", "") or checkpoint.prompt
escalation_type = payload.get("escalation_type", "decision_needed")
options = payload.get("options", [])
default_action = payload.get("default_action", "")
lines = [line.strip() for line in prompt.split("\n") if line.strip()]
title = (lines[0].lstrip("[").split("]", 1)[-1].strip() if lines else "Action Required") or "Action Required"
detail_lines = lines[1:] if len(lines) > 1 else []
header = Text()
header.append("ESCALATION", style="bold #fbbf24")
header.append(" ")
type_label = escalation_type.replace("_", " ")
header += badge(type_label, "bold black on #64748b")
parts: list[RenderableType] = [header]
if detail_lines:
body = Text()
for line in detail_lines[:10]:
body.append(f"\n{truncate_text(line, 200)}", style="white")
parts.append(body)
elif not detail_lines and title != "Action Required":
parts.append(Text(f"\n{truncate_text(title, 200)}", style="white"))
if options:
opts_text = Text("\nOptions:", style="bold #cbd5e1")
for opt in options:
opt_id = opt.get("id", "") if isinstance(opt, dict) else str(opt)
opt_label = opt.get("label", opt_id) if isinstance(opt, dict) else str(opt)
opts_text.append(f"\n \u25c6 {opt_label}", style="white")
parts.append(opts_text)
if default_action:
parts.append(Text(f"\nDefault on timeout: {default_action}", style="dim italic"))
parts.append(Text("\n[a] Approve [d] Deny [e] Feedback", style="dim italic"))
return Panel(Group(*parts), title=f"Checkpoint: {title[:40]}", border_style="#fbbf24")
# ----- Generic fallback -----
def _render_generic_checkpoint(self, checkpoint: PendingCheckpointView) -> RenderableType:
section = Text()
section.append(checkpoint.checkpoint_type.upper(), style="bold #fbbf24")
section.append(" ")
section += badge("PENDING", status_style("warn"))
if checkpoint.summary:
section.append(f"\n{truncate_text(checkpoint.summary, 120)}", style="white")
section.append(f"\n{truncate_text(checkpoint.prompt, 420)}", style="dim")
section.append("\n[a] Approve [d] Deny [e] Feedback", style="dim italic")
return Panel(section, title="Checkpoint", border_style="#fbbf24")
# ----- Shared renderers -----
def _render_linked(self) -> Text:
title = "Linked Execution Turns" if self.state.snapshot.mode == "company" else "Linked Executions"
linked_text = Text(f"\n{title}\n", style="bold #cbd5e1")
for linked in self.detail.linked_executions[:8]:
linked_text.append(f"\u2022 {truncate_text(linked.title, 40)}", style="white")
linked_text.append(f" {linked.status}", style=status_style(linked.status))
linked_text.append(f" {humanize_age(linked.updated_at)}", style="dim")
if linked.assigned_to:
linked_text.append(f" {truncate_text(linked.assigned_to, 16)}", style="dim")
linked_text.append("\n")
return linked_text
@staticmethod
def _render_section(title: str, body: str, limit: int) -> Text:
section = Text(f"\n{title}\n", style="bold #cbd5e1")
section.append(truncate_text(body, limit), style="white")
return section