1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
//
// AskView.swift
// AT-IRC
//
// Owner questions: availability toggle, pending/all list, and answering with
// an option button or free text.
//
import SwiftUI
struct AskView: View {
@Bindable var store: ChatStore
@Environment(\.dismiss) private var dismiss
@State private var showAll = false
private var visibleAsks: [AskQuestion] {
showAll ? store.asks : store.pendingAsks
}
var body: some View {
NavigationStack {
List {
availabilitySection
filterSection
questionsSection
}
.navigationTitle("Questions")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button("Done") { dismiss() }
}
}
.refreshable { await store.refreshAsks(force: true) }
.task { await store.refreshAsks(force: true) }
}
}
private var availabilitySection: some View {
Section {
Toggle(isOn: Binding(
get: { store.ownerMode != "autonomous" },
set: { available in
Task { await store.setOwnerMode(available ? "available" : "autonomous") }
}
)) {
VStack(alignment: .leading, spacing: 2) {
Text(store.ownerMode == "autonomous" ? "Away" : "Available")
Text(store.ownerMode == "autonomous"
? "Agents research and decide on their own; decisions and ADRs show up here."
: "New questions notify you and wait for your answer.")
.font(.caption)
.foregroundStyle(.secondary)
}
}
if let error = store.asksError {
Text(error)
.font(.caption)
.foregroundStyle(.red)
}
}
}
private var filterSection: some View {
Section {
Picker("Show", selection: $showAll) {
Text("Pending").tag(false)
Text("All").tag(true)
}
.pickerStyle(.segmented)
}
}
private var questionsSection: some View {
Section("Questions") {
if visibleAsks.isEmpty {
Text(showAll ? "No questions yet." : "No pending questions.")
.foregroundStyle(.secondary)
} else {
ForEach(visibleAsks) { ask in
AskRow(ask: ask, store: store)
}
}
}
}
}
private struct AskRow: View {
let ask: AskQuestion
let store: ChatStore
@State private var reply = ""
@State private var sending = false
var body: some View {
VStack(alignment: .leading, spacing: 8) {
HStack {
Text(ask.asker)
.font(.caption.weight(.semibold))
Spacer()
Text(ask.status.capitalized)
.font(.caption2)
.foregroundStyle(statusColor)
}
Text(ask.question)
.font(.body)
if let context = ask.context, !context.isEmpty {
Text(context)
.font(.caption)
.foregroundStyle(.secondary)
}
HStack(spacing: 8) {
if let channel = ask.channel, !channel.isEmpty {
Text(channel)
}
if let date = ask.date {
Text(date, format: .relative(presentation: .named))
}
}
.font(.caption2)
.foregroundStyle(.tertiary)
switch ask.status {
case "pending":
pendingControls
case "answered":
Text("Answered by \(ask.answeredBy ?? "owner"): \(ask.answer ?? "")")
.font(.callout)
case "decided":
Text("Decided by \(ask.decidedBy ?? "agent"): \(ask.decision ?? ask.answer ?? "")")
.font(.callout)
if let adr = ask.adr, !adr.isEmpty {
if let url = URL(string: adr), url.scheme != nil {
Link("ADR: \(adr)", destination: url)
.font(.caption)
} else {
Text("ADR: \(adr)")
.font(.caption2)
.foregroundStyle(.secondary)
}
}
default:
EmptyView()
}
}
.padding(.vertical, 4)
}
@ViewBuilder
private var pendingControls: some View {
if let options = ask.options, !options.isEmpty {
ForEach(Array(options.enumerated()), id: \.offset) { index, option in
Button(option) { submit(option, choice: index) }
.buttonStyle(.bordered)
.disabled(sending)
}
}
HStack {
TextField("Answer", text: $reply)
.textFieldStyle(.roundedBorder)
Button {
submit(reply, choice: nil)
} label: {
Image(systemName: "arrow.up.circle.fill")
.font(.title2)
}
.buttonStyle(.plain)
.disabled(sending || reply.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
}
}
private var statusColor: Color {
switch ask.status {
case "pending": return .orange
case "answered": return .green
case "decided": return .blue
default: return .gray
}
}
private func submit(_ answer: String, choice: Int?) {
let text = answer.trimmingCharacters(in: .whitespacesAndNewlines)
guard !text.isEmpty || choice != nil else { return }
sending = true
Task {
await store.answer(ask, answer: text, choice: choice)
sending = false
reply = ""
}
}
}