import SwiftUI /// Nearlight Activity: a working log. Plain rows on Ground, no cards; /// live updates land in place without reordering while reading. struct TaskPanelView: View { @ObservedObject var taskStore: TaskStore @Environment(\.dynamicTypeSize) private var typeSize var body: some View { Group { if taskStore.sortedTasks.isEmpty { emptyState } else { taskList } } .navigationTitle("Activity") .background(Nearlight.ground) } private var taskList: some View { ScrollView { LazyVStack(alignment: .leading, spacing: 0) { ForEach(taskStore.sortedTasks) { task in NavigationLink { TaskDetailView(taskStore: taskStore, taskID: task.id) } label: { TaskRowView(task: task) } .buttonStyle(.plain) Divider() .overlay(Nearlight.separator) } } .padding(.horizontal, Nearlight.s20) } } private var emptyState: some View { VStack(alignment: .leading, spacing: Nearlight.s8) { Image(systemName: "checklist") .font(.system(size: 28)) .foregroundStyle(Nearlight.quiet) Text("No tasks yet") .font(NearlightFont.heading(typeSize)) Text("Tasks from your conversations will appear here.") .font(.body) .foregroundStyle(Nearlight.quiet) } .padding(.top, Nearlight.s32) .padding(.horizontal, Nearlight.s20) .frame(maxWidth: .infinity, alignment: .leading) .accessibilityIdentifier("activity.empty") } } /// One activity row: status symbol, title, current step/status, chevron. private struct TaskRowView: View { let task: TaskItem @Environment(\.dynamicTypeSize) private var typeSize var body: some View { HStack(spacing: Nearlight.s12) { Image(systemName: statusSymbol) .font(.system(size: 20)) .foregroundStyle(statusColor) .accessibilityHidden(true) VStack(alignment: .leading, spacing: Nearlight.s4) { Text(statusTitle) .font(NearlightFont.support(typeSize)) .foregroundStyle(statusColor) .fixedSize(horizontal: false, vertical: true) Text(task.description) .font(NearlightFont.control(typeSize)) .foregroundStyle(Nearlight.ink) .fixedSize(horizontal: false, vertical: true) if let latest = latestStep { Text(latest) .font(NearlightFont.support(typeSize)) .foregroundStyle(Nearlight.quiet) .fixedSize(horizontal: false, vertical: true) } Text(task.createdAt, style: .time) .font(NearlightFont.metadata(typeSize)) .foregroundStyle(Nearlight.quiet) .monospacedDigit() } Spacer(minLength: 0) Image(systemName: "chevron.right") .font(.system(size: 13, weight: .medium)) .foregroundStyle(Nearlight.quiet) .accessibilityHidden(true) } .padding(.vertical, Nearlight.s16) .contentShape(Rectangle()) } private var statusTitle: String { switch task.status { case .pending: return "Queued" case .running: return "In progress" case .completed: return "Complete" case .failed: return "Needs attention" } } private var latestStep: String? { if let error = task.error { return error } if let result = task.result { return result } return task.steps.last?.content } private var statusSymbol: String { switch task.status { case .pending: return "circle.dotted" case .running: return "arrow.triangle.2.circlepath" case .completed: return "checkmark.circle" case .failed: return "exclamationmark.circle" } } private var statusColor: Color { switch task.status { case .pending: return Nearlight.quiet case .running: return Nearlight.rosewood case .completed: return Nearlight.quiet case .failed: return Nearlight.alert } } } /// Native push detail for one task: title, status, steps, result/error. /// Looks the task up live in the store so removal shows the missing state. struct TaskDetailView: View { @ObservedObject var taskStore: TaskStore let taskID: String @Environment(\.dynamicTypeSize) private var typeSize private var task: TaskItem? { taskStore.tasks[taskID] } var body: some View { Group { if let task { TaskDetailContent(task: task) } else { Text("This task is no longer available.") .font(.body) .foregroundStyle(Nearlight.quiet) .padding(Nearlight.s20) .frame(maxWidth: .infinity, alignment: .leading) } } .background(Nearlight.ground) .navigationTitle("Task") .navigationBarTitleDisplayMode(.inline) } } private struct TaskDetailContent: View { let task: TaskItem @Environment(\.dynamicTypeSize) private var typeSize var body: some View { ScrollView { VStack(alignment: .leading, spacing: Nearlight.s24) { VStack(alignment: .leading, spacing: Nearlight.s12) { Text(task.description) .font(NearlightFont.heading(typeSize)) .fixedSize(horizontal: false, vertical: true) Label(statusTitle, systemImage: statusSymbol) .font(NearlightFont.support(typeSize)) .foregroundStyle(statusColor) Text(task.createdAt, style: .time) .font(NearlightFont.metadata(typeSize)) .foregroundStyle(Nearlight.quiet) .monospacedDigit() } VStack(alignment: .leading, spacing: 0) { Text("Steps") .font(NearlightFont.heading(typeSize)) .padding(.bottom, Nearlight.s8) if task.steps.isEmpty { Text("No steps reported yet.") .font(.body) .foregroundStyle(Nearlight.quiet) } else { ForEach(task.steps) { step in VStack(alignment: .leading, spacing: Nearlight.s4) { if let role = step.role { Text(roleLabel(role)) .font(NearlightFont.support(typeSize).weight(.semibold)) .foregroundStyle(Nearlight.quiet) } Text(step.content ?? "") .font(.body) .foregroundStyle(Nearlight.ink) .textSelection(.enabled) .fixedSize(horizontal: false, vertical: true) } .padding(.vertical, Nearlight.s16) } } } if let result = task.result { Text(result) .font(.body) .foregroundStyle(Nearlight.ink) .textSelection(.enabled) } if let error = task.error { Text(error) .font(.body) .foregroundStyle(Nearlight.alert) .textSelection(.enabled) } } .padding(.horizontal, Nearlight.s20) .padding(.top, Nearlight.s24) .padding(.bottom, Nearlight.s24) } } private var statusTitle: String { switch task.status { case .pending: return "Queued" case .running: return "In progress" case .completed: return "Complete" case .failed: return "Needs attention" } } private var statusSymbol: String { switch task.status { case .pending: return "circle.dotted" case .running: return "arrow.triangle.2.circlepath" case .completed: return "checkmark.circle" case .failed: return "exclamationmark.circle" } } private var statusColor: Color { switch task.status { case .pending: return Nearlight.quiet case .running: return Nearlight.rosewood case .completed: return Nearlight.quiet case .failed: return Nearlight.alert } } private func roleLabel(_ role: String) -> String { switch role { case "thinking": return "Thinking" case "tool_call": return "Tool" default: return role } } } #Preview { NavigationStack { TaskPanelView(taskStore: TaskStore()) } }