Skip rebuilding the composer bar on unrelated transcript updates

This commit is contained in:
Ian Gulliver
2026-10-23 14:56:14 -07:00
parent 958ae31e84
commit 8b89f14694
2 changed files with 155 additions and 128 deletions
+2 -2
View File
@@ -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
View File
@@ -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,