Add team tasks: several agents in one worktree sharing a board
This commit is contained in:
@@ -13,6 +13,8 @@ final class ChatModel {
|
||||
let kind: ChatKind
|
||||
let store: Store
|
||||
let builder: TranscriptBuilder
|
||||
let member: String?
|
||||
@ObservationIgnored private var members: [String: String] = [:]
|
||||
private(set) var scrollRequest: (seq: Int, id: Int, anchor: UnitPoint)?
|
||||
private(set) var fetching = false
|
||||
private(set) var older: [ByreEvent]?
|
||||
@@ -30,6 +32,7 @@ final class ChatModel {
|
||||
self.kind = kind
|
||||
self.store = store
|
||||
let taskId: String? = if case .task(let id) = kind { id } else { nil }
|
||||
member = taskId.flatMap { store.pickedMember($0) }
|
||||
let session: () -> AgentSession? = { [weak store] in
|
||||
switch kind {
|
||||
case .lead: store?.currentLead()
|
||||
@@ -82,8 +85,26 @@ final class ChatModel {
|
||||
}
|
||||
}
|
||||
|
||||
var team: Bool {
|
||||
member != nil
|
||||
}
|
||||
|
||||
var placeholder: String {
|
||||
kind == .lead ? "Message your lead. Paste or drop images to attach them." : "Message the agent. Paste or drop images to attach them."
|
||||
if team {
|
||||
return "Post to the team's board; every agent of the team gets it."
|
||||
}
|
||||
return kind == .lead ? "Message your lead. Paste or drop images to attach them." : "Message the agent. Paste or drop images to attach them."
|
||||
}
|
||||
|
||||
private func shows(_ ev: ByreEvent) -> Bool {
|
||||
guard let member, let sid = ev.agentSessionId else {
|
||||
return true
|
||||
}
|
||||
if ev.kind == "session.created" {
|
||||
members[sid] = str(ev.payload.value, "member") ?? ""
|
||||
}
|
||||
let owner = members[sid] ?? store.sessions[taskId ?? ""]?.first { $0.id == sid }?.member ?? ""
|
||||
return owner.isEmpty || owner == member
|
||||
}
|
||||
|
||||
var empty: String {
|
||||
@@ -122,7 +143,9 @@ final class ChatModel {
|
||||
self?.appended(events)
|
||||
})
|
||||
offs.append(feed.streamed.add { [weak self] ev in
|
||||
self?.builder.appendDelta(ev)
|
||||
if self?.shows(ev) == true {
|
||||
self?.builder.appendDelta(ev)
|
||||
}
|
||||
})
|
||||
activeObserver = NotificationCenter.default.addObserver(of: NSApplication.self, for: .didBecomeActive) { [weak self] _ in
|
||||
self?.markShown()
|
||||
@@ -144,7 +167,8 @@ final class ChatModel {
|
||||
store.drafts?.flush()
|
||||
}
|
||||
|
||||
private func appended(_ events: [ByreEvent]) {
|
||||
private func appended(_ all: [ByreEvent]) {
|
||||
let events = all.filter(shows)
|
||||
if events.contains(where: { $0.kind == "agent.tool_result" || $0.kind == "turn.ended" }) {
|
||||
unmergedTimer?.cancel()
|
||||
unmergedTimer = Task { [weak self] in
|
||||
@@ -254,6 +278,13 @@ final class ChatModel {
|
||||
case .lead:
|
||||
_ = try await api.messageLead(body: .json(.init(text: text, images: images, model: sessionId != nil ? model : firstModel))).accepted
|
||||
case .task(let id):
|
||||
if team {
|
||||
if images != nil {
|
||||
throw ApiError(status: 0, message: "The board takes text only; drop the images to post.")
|
||||
}
|
||||
_ = try await api.postToBoard(path: .init(taskId: id ?? ""), body: .json(.init(text: text))).noContent
|
||||
return
|
||||
}
|
||||
if let sessionId {
|
||||
_ = try await api.sendMessage(path: .init(sessionId: sessionId), body: .json(.init(text: text, images: images, model: model))).accepted
|
||||
return
|
||||
@@ -346,8 +377,11 @@ private struct ChatHead: View {
|
||||
}
|
||||
} else {
|
||||
let sess = store.currentSession(task.id)
|
||||
let busy = sess?.state == .running
|
||||
let busy = store.taskBusy(task.id)
|
||||
header {
|
||||
if model.team {
|
||||
MemberPicker(taskId: task.id, picked: model.member)
|
||||
}
|
||||
CompactButton(sessionId: sess?.id, state: sess?.state, usage: sess.flatMap { model.builder.context[$0.id] })
|
||||
SyncButton(task: task, busy: busy)
|
||||
Spacer(minLength: 12)
|
||||
@@ -372,6 +406,23 @@ private struct ChatHead: View {
|
||||
}
|
||||
}
|
||||
|
||||
private struct MemberPicker: View {
|
||||
let taskId: String
|
||||
let picked: String?
|
||||
@Environment(Store.self) private var store
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 4) {
|
||||
ForEach(store.team(taskId), id: \.id) { s in
|
||||
PaneTabButton(label: s.state == .running ? "\(s.member) ●" : s.member, active: s.member == picked) {
|
||||
store.pickedMembers[taskId] = s.member
|
||||
}
|
||||
.help("Show \(s.member)'s transcript (\(s.model.isEmpty ? s.vendor.rawValue : s.model), \(s.state.rawValue))")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct SyncButton: View {
|
||||
let task: ByreTask
|
||||
let busy: Bool
|
||||
@@ -782,7 +833,7 @@ struct DetailsBox<Summary: View, Content: View>: View {
|
||||
}
|
||||
}
|
||||
|
||||
private struct WhenText: View {
|
||||
struct WhenText: View {
|
||||
let when: Date
|
||||
|
||||
var body: some View {
|
||||
|
||||
Reference in New Issue
Block a user