ai-dispatch 10.38.0

Multi-AI CLI team orchestrator
// Tactical scan scope — circular radar (starship) or blocky minimap (pixel).
// Exports: ContactScope.

import SwiftUI

struct ContactScope: View {
    @Environment(\.theme) private var theme
    let missions: [Mission]

    var body: some View {
        VStack(alignment: .leading, spacing: theme.spacing.sm) {
            MonoLabel(text: "\(missions.count) contacts")
            ZStack {
                if theme.motion.stepped {
                    blockScope
                } else {
                    radarScope
                }
            }
            .frame(width: 128, height: 128)
        }
    }

    private var radarScope: some View {
        ZStack {
            Circle()
                .stroke(theme.panelEdge, lineWidth: theme.hairline)
            Circle()
                .stroke(theme.panelEdge.opacity(0.5), lineWidth: theme.hairline)
                .scaleEffect(0.66)
            Circle()
                .stroke(theme.panelEdge.opacity(0.3), lineWidth: theme.hairline)
                .scaleEffect(0.33)
            ForEach(missions) { mission in
                blip(for: mission)
            }
        }
    }

    private var blockScope: some View {
        ZStack {
            Rectangle()
                .stroke(theme.panelEdge, lineWidth: theme.hairline)
            Path { path in
                path.move(to: CGPoint(x: 0, y: 64))
                path.addLine(to: CGPoint(x: 128, y: 64))
                path.move(to: CGPoint(x: 64, y: 0))
                path.addLine(to: CGPoint(x: 64, y: 128))
            }
            .stroke(theme.panelEdge.opacity(0.5), lineWidth: theme.hairline)
            ForEach(missions) { mission in
                blip(for: mission)
            }
        }
    }

    @ViewBuilder
    private func blip(for mission: Mission) -> some View {
        let placement = blipPlacement(for: mission.id, progress: mission.progress)
        let color = blipColor(for: mission.state)
        if theme.motion.stepped {
            Rectangle()
                .fill(color)
                .frame(width: 6, height: 6)
                .offset(x: placement.x - 64, y: placement.y - 64)
        } else {
            Circle()
                .fill(color)
                .frame(width: mission.state == .run ? 8 : 6, height: mission.state == .run ? 8 : 6)
                .shadow(color: color.opacity(0.6), radius: mission.state == .run ? 4 : 0)
                .offset(x: placement.x - 64, y: placement.y - 64)
                .opacity(mission.state == .run ? 1 : 0.85)
        }
    }

    private func blipColor(for state: MissionDisplayState) -> Color {
        switch state {
        case .run: return theme.run
        case .done: return theme.done
        case .fail: return theme.fail
        case .stop: return theme.stop
        }
    }

    private func blipPlacement(for id: MissionID, progress: Double) -> CGPoint {
        let hash = abs(id.hashValue)
        let angle = Double(hash % 360) * .pi / 180
        let radius = 20 + (1 - progress) * 40
        let x = 64 + cos(angle) * radius
        let y = 64 + sin(angle) * radius
        return CGPoint(x: x, y: y)
    }
}