import SwiftUI import NucleicProtocol /// The to-do inbox (mirrors the Mac's idea inbox): capture ideas, group by project, then /// complete, dispatch (start an agent), or delete each — all control scope. struct TodosView: View { @EnvironmentObject var store: RemoteStore @State private var draft = "" private var open: [WireTodo] { store.dashboard.todos.filter { $0.status != .done } } /// Group open todos by project name ("Unassigned" bucket last). private var groups: [(name: String, todos: [WireTodo])] { let byProject = Dictionary(grouping: open) { $0.projectName ?? "Unassigned" } return byProject .map { ($0.key, $0.value.sorted { $0.updatedAt > $1.updatedAt }) } .sorted { ($0.name == "Unassigned" ? "zzz" : $0.name) < ($1.name == "Unassigned" ? "zzz" : $1.name) } } var body: some View { NavigationStack { List { Section { HStack { TextField("Capture an idea…", text: $draft) .textFieldStyle(.roundedBorder) .onSubmit(capture) .keyboardDismissable() Button(action: capture) { Image(systemName: "plus.circle.fill").font(.title3) } .disabled(draft.trimmingCharacters(in: .whitespaces).isEmpty || !store.canControl) } } if open.isEmpty { ContentUnavailableView("Inbox zero", systemImage: "checkmark.circle", description: Text("Capture an idea above to dispatch later.")) } ForEach(groups, id: \.name) { group in Section(group.name.uppercased()) { ForEach(group.todos) { todo in TodoRow(todo: todo) } } } } .navigationTitle("To-dos") .scrollDismissesKeyboard(.interactively) .refreshable { store.refreshSessions() } } } private func capture() { store.captureTodo(draft, projectID: nil) draft = "" } } private struct TodoRow: View { @EnvironmentObject var store: RemoteStore let todo: WireTodo var body: some View { HStack(spacing: 10) { Button { store.completeTodo(todo.id) } label: { Image(systemName: todo.status == .dispatched ? "circle.dashed" : "circle") .foregroundStyle(.secondary) }.buttonStyle(.plain) VStack(alignment: .leading, spacing: 2) { Text(todo.summary?.isEmpty == false ? todo.summary! : todo.text) .font(.subheadline).lineLimit(2) HStack(spacing: 6) { if let triage = todo.triage { TriageBadge(rawLevel: triage) } if todo.status == .dispatched { Text("Dispatched").font(.caption2).foregroundStyle(Palette.active) } } } Spacer() dispatchButton } .swipeActions { Button(role: .destructive) { store.deleteTodo(todo.id) } label: { Label("Delete", systemImage: "trash") } } } /// Dispatch to the todo's project, or pick one if unassigned. @ViewBuilder private var dispatchButton: some View { if todo.status == .open && store.canControl { if let pid = todo.projectID { Button { store.dispatchTodo(todo.id, in: pid) } label: { Image(systemName: "play.circle.fill").foregroundStyle(Palette.accent) }.buttonStyle(.plain) } else if !store.dashboard.projects.isEmpty { Menu { ForEach(store.dashboard.projects) { project in Button(project.name) { store.dispatchTodo(todo.id, in: project.id) } } } label: { Image(systemName: "play.circle").foregroundStyle(Palette.accent) } } } } } /// A small colored badge showing an idea's triage level — the Mac's `TriageBadge`, fed by /// the wire's raw level name. Honors the encouraging/classic labeling setting (same storage /// key as the Mac); an unrecognized level from a newer host renders no badge. struct TriageBadge: View { let rawLevel: String @AppStorage("nucleic.triageEncouragingLabels") private var encouraging = true private var label: String? { switch (rawLevel, encouraging) { case ("critical", true): "Max Impact" case ("high", true): "High Impact" case ("medium", true): "Worthwhile" case ("low", true): "Someday" case ("critical", false): "Critical" case ("high", false): "High" case ("medium", false): "Medium" case ("low", false): "Low" default: nil } } /// The Mac's badge hues: a red→orange→yellow→gray urgency scale for classic labels, a /// magenta→blue→green→slate reward scale for the encouraging framing. private var tint: Color? { switch (rawLevel, encouraging) { case ("critical", false): .red case ("high", false): .orange case ("medium", false): .yellow case ("low", false): .gray case ("critical", true): Color(red: 0.95, green: 0.33, blue: 0.78) // vivid magenta case ("high", true): Color(red: 0.26, green: 0.54, blue: 1.00) // bright blue case ("medium", true): Color(red: 0.18, green: 0.78, blue: 0.45) // green case ("low", true): Color(red: 0.56, green: 0.60, blue: 0.67) // calm slate default: nil } } var body: some View { if let label, let tint { HStack(spacing: 4) { Circle().fill(tint).frame(width: 7, height: 7) Text(label) .font(.caption2.weight(.semibold)) .foregroundStyle(.secondary) } .padding(.horizontal, 7).padding(.vertical, 3) .background(tint.opacity(0.14), in: .capsule) .overlay(Capsule().strokeBorder(tint.opacity(0.35), lineWidth: 0.5)) .fixedSize() } } }