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."
|
||||
|
||||
struct AttentionGroup: Identifiable {
|
||||
struct AttentionGroup: Identifiable, Equatable {
|
||||
let id: String
|
||||
let label: String
|
||||
let help: String
|
||||
let rows: [Int]
|
||||
}
|
||||
|
||||
struct ChatStatus {
|
||||
struct ChatStatus: Equatable {
|
||||
enum Tone {
|
||||
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 {
|
||||
let chat: ChatModel
|
||||
let key: String
|
||||
@@ -654,45 +792,23 @@ struct ComposerView: View {
|
||||
Thumbnails(key: draftKey)
|
||||
editor
|
||||
}
|
||||
let status = chat.status().map { s in
|
||||
StatusButton(status: s) {
|
||||
chat.showStatus(s)
|
||||
}
|
||||
}
|
||||
let buttons = actions(live?.id, running: sessionState == .running, paused: live?.paused ?? false)
|
||||
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
|
||||
}
|
||||
ComposerBar(chat: chat, status: chat.status(), shown: shown, draftKey: draftKey, modelKey: m.modelKey, options: m.options, selected: m.selected,
|
||||
selectedEffort: m.selectedEffort, sessionId: live?.id, running: sessionState == .running, paused: live?.paused ?? false, closed: chat.closed) {
|
||||
if shown {
|
||||
shownOverride = false
|
||||
} else {
|
||||
reveal()
|
||||
}
|
||||
} 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(.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 {
|
||||
AgentEditor(text: $text, placeholder: chat.placeholder, key: draftKey, height: touchOnly ? 72 : 104,
|
||||
focus: $focused, border: inTask ? accent : focused ? Palette.accent : Palette.border,
|
||||
|
||||
Reference in New Issue
Block a user