Design system: Palette (teal accent + the Mac's categorical status colors, status→color refined by turn disposition), StatusStyle glyphs/labels, a card surface modifier, app-wide teal tint. Fixes the reported bug: Sessions now hides archived chats and uses disposition to split NEEDS YOU from DONE (a finished-the-work turn no longer shows as needing you). Rows gain status dots, favorite star, approval-count badge, diffstat; swipe to favorite/archive/delete. New surfaces, full control-scope parity with the Mac: - Home: greeting + day-streak flame, GitHub-style activity grid, stat cards, quick to-dos, and a start-chat composer (project picker + Auto). - Projects: per-project session counts → project detail with a scoped composer. - To-dos: capture, group by project, complete/dispatch/delete. - Session detail: a control menu (rename, favorite, interrupt, integrate merge/squash/rebase, archive, delete). RemoteStore handles the dashboard event + exposes all control intents; claims control scope. A NUCLEIC_DEMO/NUCLEIC_TAB env seam seeds mock state for offline UI preview. iOS app builds for the iOS 27 simulator; Home + Sessions verified rendering via screenshots. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
100 lines
3.8 KiB
Swift
100 lines
3.8 KiB
Swift
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)
|
|
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")
|
|
.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)
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|