Skip rebuilding the composer bar on unrelated transcript updates
This commit is contained in:
@@ -28,14 +28,14 @@ Start in [Settings](byre-app://tab?name=settings): \(leadSettings)
|
|||||||
|
|
||||||
private let leadSetup = "Walk me through setting up byre: the tools its agents need, signing in to their vendors, and the macOS permissions on the server's Mac and on my Macs."
|
private let leadSetup = "Walk me through setting up byre: the tools its agents need, signing in to their vendors, and the macOS permissions on the server's Mac and on my Macs."
|
||||||
|
|
||||||
struct AttentionGroup: Identifiable {
|
struct AttentionGroup: Identifiable, Equatable {
|
||||||
let id: String
|
let id: String
|
||||||
let label: String
|
let label: String
|
||||||
let help: String
|
let help: String
|
||||||
let rows: [Int]
|
let rows: [Int]
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ChatStatus {
|
struct ChatStatus: Equatable {
|
||||||
enum Tone {
|
enum Tone {
|
||||||
case attention, working, merged, discarded
|
case attention, working, merged, discarded
|
||||||
}
|
}
|
||||||
|
|||||||
+153
-126
@@ -562,6 +562,144 @@ struct AgentEditor: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private struct ComposerBar: View, Equatable {
|
||||||
|
let chat: ChatModel
|
||||||
|
let status: ChatStatus?
|
||||||
|
let shown: Bool
|
||||||
|
let draftKey: String
|
||||||
|
let modelKey: String
|
||||||
|
let options: [String]
|
||||||
|
let selected: String
|
||||||
|
let selectedEffort: String
|
||||||
|
let sessionId: String?
|
||||||
|
let running: Bool
|
||||||
|
let paused: Bool
|
||||||
|
let closed: Bool
|
||||||
|
let toggle: () -> Void
|
||||||
|
let choose: @MainActor @Sendable (String) -> Void
|
||||||
|
let chooseEffort: @MainActor @Sendable (String) -> Void
|
||||||
|
let submit: () -> Void
|
||||||
|
@Environment(Overlays.self) private var overlays
|
||||||
|
|
||||||
|
// Leaves out the closures: they read their state when they run.
|
||||||
|
nonisolated static func == (a: ComposerBar, b: ComposerBar) -> Bool {
|
||||||
|
a.chat === b.chat && a.status == b.status && a.shown == b.shown && a.draftKey == b.draftKey && a.modelKey == b.modelKey && a.options == b.options
|
||||||
|
&& a.selected == b.selected && a.selectedEffort == b.selectedEffort && a.sessionId == b.sessionId && a.running == b.running && a.paused == b.paused
|
||||||
|
&& a.closed == b.closed
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
let status = status.map { s in
|
||||||
|
StatusButton(status: s) {
|
||||||
|
chat.showStatus(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let buttons = actions
|
||||||
|
ViewThatFits(in: .horizontal) {
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
controls()
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
status
|
||||||
|
buttons
|
||||||
|
.frame(maxWidth: .infinity, alignment: .trailing)
|
||||||
|
}
|
||||||
|
VStack(alignment: .trailing, spacing: 8) {
|
||||||
|
status
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
controls()
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
buttons
|
||||||
|
}
|
||||||
|
}
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
controls()
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
Spacer(minLength: 0)
|
||||||
|
status
|
||||||
|
buttons
|
||||||
|
}
|
||||||
|
}
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
controls(stacked: true)
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
Spacer(minLength: 0)
|
||||||
|
status
|
||||||
|
buttons
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func controls(stacked: Bool = false) -> some View {
|
||||||
|
(stacked ? AnyLayout(VStackLayout(alignment: .leading, spacing: 8)) : AnyLayout(HStackLayout(spacing: 8))) {
|
||||||
|
Button(action: toggle) {
|
||||||
|
Image(systemName: shown ? "chevron.down" : "chevron.up")
|
||||||
|
.frame(height: 14)
|
||||||
|
}
|
||||||
|
.buttonStyle(WebButtonStyle())
|
||||||
|
.help(shown ? "Hide the message box" : "Show the message box")
|
||||||
|
#if os(iOS)
|
||||||
|
ComposerMenu(key: draftKey, photos: shown, models: chat.team ? [] : options, selected: selected, effort: selectedEffort, choose: choose, chooseEffort: chooseEffort)
|
||||||
|
#else
|
||||||
|
if !chat.team {
|
||||||
|
Picker("", selection: Binding(get: { selected }, set: choose)) {
|
||||||
|
ForEach(options, id: \.self) { option in
|
||||||
|
Text(option).tag(option)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.labelsHidden()
|
||||||
|
.pickerStyle(.menu)
|
||||||
|
.fixedSize()
|
||||||
|
.help("Model for the next turn")
|
||||||
|
Picker("", selection: Binding(get: { selectedEffort }, set: chooseEffort)) {
|
||||||
|
ForEach(efforts(selected), id: \.self) { option in
|
||||||
|
Text(effortLabel(option)).tag(option)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.labelsHidden()
|
||||||
|
.pickerStyle(.menu)
|
||||||
|
.fixedSize()
|
||||||
|
.help("Effort for the next turn")
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var actions: some View {
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
Button {
|
||||||
|
if let sessionId {
|
||||||
|
chat.interrupt(sessionId, overlays)
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "stop.fill")
|
||||||
|
.frame(width: 14, height: 14)
|
||||||
|
}
|
||||||
|
.buttonStyle(WebButtonStyle(danger: true))
|
||||||
|
.disabled(!running)
|
||||||
|
.help("Interrupt the agent's turn")
|
||||||
|
if case .task = chat.kind, !chat.team, !closed {
|
||||||
|
Button {
|
||||||
|
if let sessionId {
|
||||||
|
chat.setPaused(sessionId, !paused, overlays)
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Image(systemName: paused ? "play.fill" : "pause.fill")
|
||||||
|
.frame(width: 14, height: 14)
|
||||||
|
}
|
||||||
|
.buttonStyle(WebButtonStyle())
|
||||||
|
.disabled(sessionId == nil)
|
||||||
|
.help(paused ? "Resume the agent" : "Pause the agent after its current step")
|
||||||
|
}
|
||||||
|
Button(closed ? "Continue" : "Send", action: submit)
|
||||||
|
.buttonStyle(WebButtonStyle())
|
||||||
|
.disabled(!shown)
|
||||||
|
.fixedSize()
|
||||||
|
.help(closed ? "Start a new task from the default branch whose agent continues this conversation with your message" : "Send the message")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct ComposerView: View {
|
struct ComposerView: View {
|
||||||
let chat: ChatModel
|
let chat: ChatModel
|
||||||
let key: String
|
let key: String
|
||||||
@@ -654,45 +792,23 @@ struct ComposerView: View {
|
|||||||
Thumbnails(key: draftKey)
|
Thumbnails(key: draftKey)
|
||||||
editor
|
editor
|
||||||
}
|
}
|
||||||
let status = chat.status().map { s in
|
ComposerBar(chat: chat, status: chat.status(), shown: shown, draftKey: draftKey, modelKey: m.modelKey, options: m.options, selected: m.selected,
|
||||||
StatusButton(status: s) {
|
selectedEffort: m.selectedEffort, sessionId: live?.id, running: sessionState == .running, paused: live?.paused ?? false, closed: chat.closed) {
|
||||||
chat.showStatus(s)
|
if shown {
|
||||||
}
|
shownOverride = false
|
||||||
}
|
} else {
|
||||||
let buttons = actions(live?.id, running: sessionState == .running, paused: live?.paused ?? false)
|
reveal()
|
||||||
ViewThatFits(in: .horizontal) {
|
|
||||||
HStack(spacing: 8) {
|
|
||||||
controls(m)
|
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
|
||||||
status
|
|
||||||
buttons
|
|
||||||
.frame(maxWidth: .infinity, alignment: .trailing)
|
|
||||||
}
|
|
||||||
VStack(alignment: .trailing, spacing: 8) {
|
|
||||||
status
|
|
||||||
HStack(spacing: 8) {
|
|
||||||
controls(m)
|
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
|
||||||
buttons
|
|
||||||
}
|
|
||||||
}
|
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
|
||||||
controls(m)
|
|
||||||
HStack(spacing: 8) {
|
|
||||||
Spacer(minLength: 0)
|
|
||||||
status
|
|
||||||
buttons
|
|
||||||
}
|
|
||||||
}
|
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
|
||||||
controls(m, stacked: true)
|
|
||||||
HStack(spacing: 8) {
|
|
||||||
Spacer(minLength: 0)
|
|
||||||
status
|
|
||||||
buttons
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
} choose: { option in
|
||||||
|
pick = option
|
||||||
|
modelChoices[m.modelKey] = option
|
||||||
|
} chooseEffort: { option in
|
||||||
|
effortPick = option
|
||||||
|
effortChoices[m.modelKey] = option
|
||||||
|
} submit: {
|
||||||
|
submit(modelState())
|
||||||
}
|
}
|
||||||
|
.equatable()
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 16)
|
.padding(.horizontal, 16)
|
||||||
.padding(.vertical, 12)
|
.padding(.vertical, 12)
|
||||||
@@ -748,95 +864,6 @@ struct ComposerView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func controls(_ m: ModelState, stacked: Bool = false) -> some View {
|
|
||||||
(stacked ? AnyLayout(VStackLayout(alignment: .leading, spacing: 8)) : AnyLayout(HStackLayout(spacing: 8))) {
|
|
||||||
Button {
|
|
||||||
if shown {
|
|
||||||
shownOverride = false
|
|
||||||
} else {
|
|
||||||
reveal()
|
|
||||||
}
|
|
||||||
} label: {
|
|
||||||
Image(systemName: shown ? "chevron.down" : "chevron.up")
|
|
||||||
.frame(height: 14)
|
|
||||||
}
|
|
||||||
.buttonStyle(WebButtonStyle())
|
|
||||||
.help(shown ? "Hide the message box" : "Show the message box")
|
|
||||||
#if os(iOS)
|
|
||||||
ComposerMenu(key: draftKey, photos: shown, models: chat.team ? [] : m.options, selected: m.selected, effort: m.selectedEffort) { option in
|
|
||||||
pick = option
|
|
||||||
modelChoices[m.modelKey] = option
|
|
||||||
} chooseEffort: { option in
|
|
||||||
effortPick = option
|
|
||||||
effortChoices[m.modelKey] = option
|
|
||||||
}
|
|
||||||
#else
|
|
||||||
if !chat.team {
|
|
||||||
Picker("", selection: Binding(get: { m.selected }, set: { v in
|
|
||||||
pick = v
|
|
||||||
modelChoices[m.modelKey] = v
|
|
||||||
})) {
|
|
||||||
ForEach(m.options, id: \.self) { option in
|
|
||||||
Text(option).tag(option)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.labelsHidden()
|
|
||||||
.pickerStyle(.menu)
|
|
||||||
.fixedSize()
|
|
||||||
.help("Model for the next turn")
|
|
||||||
Picker("", selection: Binding(get: { m.selectedEffort }, set: { v in
|
|
||||||
effortPick = v
|
|
||||||
effortChoices[m.modelKey] = v
|
|
||||||
})) {
|
|
||||||
ForEach(efforts(m.selected), id: \.self) { option in
|
|
||||||
Text(effortLabel(option)).tag(option)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.labelsHidden()
|
|
||||||
.pickerStyle(.menu)
|
|
||||||
.fixedSize()
|
|
||||||
.help("Effort for the next turn")
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func actions(_ sessionId: String?, running: Bool, paused: Bool) -> some View {
|
|
||||||
HStack(spacing: 8) {
|
|
||||||
Button {
|
|
||||||
if let sessionId {
|
|
||||||
chat.interrupt(sessionId, overlays)
|
|
||||||
}
|
|
||||||
} label: {
|
|
||||||
Image(systemName: "stop.fill")
|
|
||||||
.frame(width: 14, height: 14)
|
|
||||||
}
|
|
||||||
.buttonStyle(WebButtonStyle(danger: true))
|
|
||||||
.disabled(!running)
|
|
||||||
.help("Interrupt the agent's turn")
|
|
||||||
if case .task = chat.kind, !chat.team, !chat.closed {
|
|
||||||
Button {
|
|
||||||
if let sessionId {
|
|
||||||
chat.setPaused(sessionId, !paused, overlays)
|
|
||||||
}
|
|
||||||
} label: {
|
|
||||||
Image(systemName: paused ? "play.fill" : "pause.fill")
|
|
||||||
.frame(width: 14, height: 14)
|
|
||||||
}
|
|
||||||
.buttonStyle(WebButtonStyle())
|
|
||||||
.disabled(sessionId == nil)
|
|
||||||
.help(paused ? "Resume the agent" : "Pause the agent after its current step")
|
|
||||||
}
|
|
||||||
Button(chat.closed ? "Continue" : "Send") {
|
|
||||||
submit(modelState())
|
|
||||||
}
|
|
||||||
.buttonStyle(WebButtonStyle())
|
|
||||||
.disabled(!shown)
|
|
||||||
.fixedSize()
|
|
||||||
.help(chat.closed ? "Start a new task from the default branch whose agent continues this conversation with your message" : "Send the message")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var editor: some View {
|
private var editor: some View {
|
||||||
AgentEditor(text: $text, placeholder: chat.placeholder, key: draftKey, height: touchOnly ? 72 : 104,
|
AgentEditor(text: $text, placeholder: chat.placeholder, key: draftKey, height: touchOnly ? 72 : 104,
|
||||||
focus: $focused, border: inTask ? accent : focused ? Palette.accent : Palette.border,
|
focus: $focused, border: inTask ? accent : focused ? Palette.accent : Palette.border,
|
||||||
|
|||||||
Reference in New Issue
Block a user