darq 0.1.0

darq CLI + TUI — autonomous issue → PR pipeline with SAT and a learning loop.
Documentation
//! Secondary panels — rendered into the right column when a toggle mode is active.
//!
//! Phase 6 (toggles): SatDials (default) | Shifts | Blueprints | Backlog | Judges.
//! Each renders a focused, single-purpose view; pressing the toggle key again
//! returns to SatDials. SatPersonaScore events auto-return from Blueprints.

use ratatui::Frame;
use ratatui::layout::Rect;
use ratatui::style::{Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Borders, Paragraph};

use crate::tui::app::{App, BlueprintKind, PanelMode};
use crate::tui::theme::Palette;

pub fn render(frame: &mut Frame, area: Rect, app: &App, palette: &Palette) {
    match app.mode {
        PanelMode::SatDials => super::dials::render(frame, area, app, palette),
        PanelMode::Shifts => render_shifts(frame, area, app, palette),
        PanelMode::Blueprints => render_blueprints(frame, area, app, palette),
        PanelMode::Backlog => render_backlog(frame, area, app, palette),
        PanelMode::Judges => render_judges(frame, area, app, palette),
    }
}

// ── Shifts panel ───────────────────────────────────────────────────────────

fn render_shifts(frame: &mut Frame, area: Rect, app: &App, palette: &Palette) {
    let total = app.runs.len() + app.approvals.len();
    let active = app
        .runs
        .iter()
        .filter(|r| r.status == "running" || r.status == "awaiting_approval")
        .count()
        + app.approvals.len();

    let block = Block::new()
        .borders(Borders::ALL)
        .border_style(Style::new().fg(palette.rule))
        .title(Span::styled(
            format!(" ACTIVE SHIFTS · {active} live · {total} total "),
            Style::new().fg(palette.fg_2).add_modifier(Modifier::BOLD),
        ));
    let inner = block.inner(area);
    frame.render_widget(block, area);

    let mut lines: Vec<Line> = Vec::new();
    for r in app.runs.iter().chain(app.approvals.iter()) {
        let glyph_color = match r.status.as_str() {
            "running" => palette.ember,
            "awaiting_approval" => palette.amber,
            "completed" | "merged" => palette.state_pass,
            "failed" | "cooled" => palette.red,
            _ => palette.fg_3,
        };
        let glyph = match r.status.as_str() {
            "running" => "",
            "awaiting_approval" => "",
            "completed" | "merged" => "",
            "failed" | "cooled" => "",
            _ => "",
        };
        let id_short = &r.id[..8.min(r.id.len())];
        lines.push(Line::from(vec![
            Span::styled(format!(" {glyph} "), Style::new().fg(glyph_color)),
            Span::styled(format!("{id_short:8}"), Style::new().fg(palette.cyan)),
            Span::raw("  "),
            Span::styled(r.issue.clone(), Style::new().fg(palette.fg_1)),
            Span::styled("  ", Style::new()),
            Span::styled(r.duration.clone(), Style::new().fg(palette.fg_3)),
        ]));
    }

    if lines.is_empty() {
        lines.push(Line::styled(
            " no shifts yet — start with `darq run issue <N>`",
            Style::new().fg(palette.fg_3),
        ));
    }

    let para = Paragraph::new(lines).style(Style::new().bg(palette.bg_0));
    frame.render_widget(para, inner);
}

// ── Blueprints panel ───────────────────────────────────────────────────────

fn render_blueprints(frame: &mut Frame, area: Rect, app: &App, palette: &Palette) {
    let block = Block::new()
        .borders(Borders::ALL)
        .border_style(Style::new().fg(palette.rule))
        .title(Span::styled(
            format!(" BLUEPRINTS · {} matches ", app.blueprints.len()),
            Style::new().fg(palette.fg_2).add_modifier(Modifier::BOLD),
        ));
    let inner = block.inner(area);
    frame.render_widget(block, area);

    let mut lines: Vec<Line> = Vec::new();
    for b in &app.blueprints {
        let kind_style = match b.kind {
            BlueprintKind::Reuse => Style::new()
                .fg(palette.bg_0)
                .bg(palette.copper)
                .add_modifier(Modifier::BOLD),
            BlueprintKind::Avoid => Style::new()
                .fg(palette.bg_0)
                .bg(palette.amber)
                .add_modifier(Modifier::BOLD),
        };
        let kind_label = match b.kind {
            BlueprintKind::Reuse => " REUSE ",
            BlueprintKind::Avoid => " AVOID ",
        };
        let short = &b.run_id[..8.min(b.run_id.len())];
        lines.push(Line::from(vec![
            Span::styled(kind_label, kind_style),
            Span::raw(" "),
            Span::styled(format!("#{short}"), Style::new().fg(palette.cyan)),
            Span::raw(" · "),
            Span::styled(b.utility_name.clone(), Style::new().fg(palette.fg_1)),
            Span::raw("  "),
            Span::styled(
                format!("{:.2}", b.similarity),
                Style::new().fg(palette.fg_2),
            ),
        ]));
    }
    if lines.is_empty() {
        lines.push(Line::styled(
            " no blueprints injected for the focused shift yet",
            Style::new().fg(palette.fg_3),
        ));
    }

    let para = Paragraph::new(lines).style(Style::new().bg(palette.bg_0));
    frame.render_widget(para, inner);
}

// ── Backlog panel ──────────────────────────────────────────────────────────

fn render_backlog(frame: &mut Frame, area: Rect, app: &App, palette: &Palette) {
    let block = Block::new()
        .borders(Borders::ALL)
        .border_style(Style::new().fg(palette.rule))
        .title(Span::styled(
            format!(
                " BACKLOG · MILESTONE {} ",
                app.milestone_name.as_deref().unwrap_or("")
            ),
            Style::new().fg(palette.fg_2).add_modifier(Modifier::BOLD),
        ));
    let inner = block.inner(area);
    frame.render_widget(block, area);

    // For v1, derive backlog items from app.runs by status. A real Method::Sweep
    // integration would replace this with milestone-issue-tracker data.
    let mut lines: Vec<Line> = Vec::new();
    if app.runs.is_empty() && app.approvals.is_empty() {
        lines.push(Line::styled(
            " no backlog items — connect to daemon and trigger `darq sweep <milestone>`",
            Style::new().fg(palette.fg_3),
        ));
    } else {
        for r in app.runs.iter().chain(app.approvals.iter()) {
            let (chip_label, chip_color) = match r.status.as_str() {
                "running" => ("[ACTIVE]", palette.ember),
                "awaiting_approval" => ("[QUEUED]", palette.amber),
                "completed" | "merged" => ("[MERGED]", palette.state_pass),
                "failed" | "cooled" => ("[STALLED]", palette.red),
                _ => ("[—]", palette.fg_3),
            };
            lines.push(Line::from(vec![
                Span::styled(" ", Style::new()),
                Span::styled(r.issue.clone(), Style::new().fg(palette.fg_1)),
                Span::styled("  ", Style::new()),
                Span::styled(
                    chip_label,
                    Style::new().fg(chip_color).add_modifier(Modifier::BOLD),
                ),
            ]));
        }
    }

    let para = Paragraph::new(lines).style(Style::new().bg(palette.bg_0));
    frame.render_widget(para, inner);
}

// ── Judges panel (stubbed for v1) ──────────────────────────────────────────

fn render_judges(frame: &mut Frame, area: Rect, app: &App, palette: &Palette) {
    let block = Block::new()
        .borders(Borders::ALL)
        .border_style(Style::new().fg(palette.rule))
        .title(Span::styled(
            " JUDGES · ensemble (stub) ",
            Style::new().fg(palette.fg_2).add_modifier(Modifier::BOLD),
        ));
    let inner = block.inner(area);
    frame.render_widget(block, area);

    let mut lines: Vec<Line> = Vec::new();
    let judges = [
        (
            "JUDGE 1",
            "ensemble · gpt-tier",
            app.sat_scores.get("junior"),
        ),
        (
            "JUDGE 2",
            "ensemble · claude-tier",
            app.sat_scores.get("senior"),
        ),
        (
            "JUDGE 3",
            "running · arbitration",
            app.sat_scores.get("maintainer"),
        ),
    ];
    for (name, attribution, state) in judges {
        let score = state
            .map(|s| format!("{:.1}", s.target))
            .unwrap_or_else(|| "".into());
        lines.push(Line::from(vec![
            Span::styled(format!(" {name} "), Style::new().fg(palette.fg_2)),
            Span::styled("· ", Style::new().fg(palette.fg_4)),
            Span::styled(attribution, Style::new().fg(palette.violet)),
            Span::styled("  ", Style::new()),
            Span::styled(score, Style::new().fg(palette.fg_0)),
        ]));
    }
    lines.push(Line::raw(""));
    lines.push(Line::styled(
        " stub: real per-judge attribution requires SAT engine extension",
        Style::new().fg(palette.fg_3),
    ));
    lines.push(Line::styled(
        " (TODO: wire from sat.rs judge enumeration)",
        Style::new().fg(palette.fg_4),
    ));

    let para = Paragraph::new(lines).style(Style::new().bg(palette.bg_0));
    frame.render_widget(para, inner);
}